| |
| """ |
| Frox AI — Serve the Morph API |
| |
| Launches the FastAPI server every client (frox-chat, frox-code, a |
| future CLI, a future Android app) connects to. |
| |
| Usage: |
| python scripts/serve.py --family classic --model ./frox-morph-1-1-output/classic_sft_final |
| python scripts/serve.py --family nano # untrained — proves the wiring works |
| python scripts/serve.py --family classic --model ./ckpt --quantization 4bit --port 8080 |
| |
| Then point any client at http://<this-host>:<port>/v1 as an |
| OpenAI-compatible endpoint. For frox-chat (Open WebUI): Settings → |
| Connections → add an OpenAI API connection with that base URL. |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import os |
| import sys |
| from pathlib import Path |
|
|
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) |
|
|
| from utils.common import print_banner, FAMILY_TIERS |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="Serve the Frox AI Morph API") |
| parser.add_argument("--family", choices=list(FAMILY_TIERS), default="classic", |
| help="Which Morph model-family tier to serve") |
| parser.add_argument("--model", type=str, default=None, |
| help="Path to a trained checkpoint. Omit to serve an " |
| "untrained model (useful only for testing the wiring).") |
| parser.add_argument("--quantization", choices=["4bit", "8bit"], default=None) |
| parser.add_argument("--host", type=str, default="0.0.0.0") |
| parser.add_argument("--port", type=int, default=8000) |
| parser.add_argument("--cors-origins", type=str, default="*", |
| help="Comma-separated allowed origins, e.g. " |
| "'http://localhost:3000,tauri://localhost'. " |
| "Defaults to '*' (fine for local dev, not for production).") |
| parser.add_argument("--memory-path", type=str, default="./data/memories.json") |
| parser.add_argument("--reload", action="store_true", help="Auto-reload on code changes (dev only)") |
|
|
| conductor_group = parser.add_argument_group("Conductor (Fugu-inspired orchestration)") |
| conductor_group.add_argument("--no-conductor", action="store_true", |
| help="Disable the 'conductor' pseudo-model entirely") |
| conductor_group.add_argument("--conductor-mode", choices=["auto", "fast", "deep"], default="auto", |
| help="Default orchestration depth when a client requests model=conductor") |
| conductor_group.add_argument("--worker", action="append", default=[], metavar="NAME=URL", |
| help="Register a remote worker for true multi-tier orchestration, e.g. " |
| "--worker code=http://localhost:8001/v1 (repeatable). Omit to use " |
| "self-orchestration on this process's one loaded model instead.") |
| args = parser.parse_args() |
|
|
| os.environ["MORPH_FAMILY"] = args.family |
| if args.model: |
| os.environ["MORPH_CHECKPOINT"] = args.model |
| if args.quantization: |
| os.environ["MORPH_QUANTIZATION"] = args.quantization |
| os.environ["MORPH_CORS_ORIGINS"] = args.cors_origins |
| os.environ["MORPH_MEMORY_PATH"] = args.memory_path |
| os.environ["MORPH_CONDUCTOR_ENABLED"] = "false" if args.no_conductor else "true" |
| os.environ["MORPH_CONDUCTOR_MODE"] = args.conductor_mode |
| for entry in args.worker: |
| if "=" not in entry: |
| parser.error(f"--worker expects NAME=URL, got: {entry!r}") |
| name, url = entry.split("=", 1) |
| os.environ[f"MORPH_WORKER_{name.strip().upper()}_URL"] = url.strip() |
|
|
| print_banner() |
| print(f"Serving Morph ({args.family}) at http://{args.host}:{args.port}") |
| print(f" OpenAI-compatible base URL for clients: http://{args.host}:{args.port}/v1") |
| if not args.model: |
| print(" âš No --model given: serving an UNTRAINED model. Responses will be gibberish.") |
| print() |
|
|
| import uvicorn |
| uvicorn.run( |
| "api.server:app", host=args.host, port=args.port, |
| reload=args.reload, log_level="info", |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|