| from __future__ import annotations
|
|
|
| import json
|
| import os
|
| import tempfile
|
| import urllib.error
|
| import urllib.request
|
| from pathlib import Path
|
| from typing import Any
|
|
|
| try:
|
| from tinyluminax.products._ui_server import serve_dashboard
|
| except ModuleNotFoundError:
|
| from ._ui_server import serve_dashboard
|
|
|
| from .gateway import route_chat_payload
|
| from .platform import build_models_api, route_or_chat_payload, route_receipt
|
| from .registry import load_model_registry
|
| from .ui import (
|
| PRODUCT_NAME,
|
| build_dashboard_state,
|
| build_expanded_dashboard_html,
|
| default_openai_chat_request_path,
|
| default_registry_path,
|
| handle_api_request,
|
| load_json_mapping,
|
| )
|
|
|
| PACKAGE_ROOT = Path(__file__).resolve().parent
|
| PACKAGE_PARENT = PACKAGE_ROOT.parent
|
|
|
| DEFAULT_GATEWAY_CONFIG: dict[str, Any] = {
|
| "mode": "route_only",
|
| "prompt_retention": "not_stored_by_default",
|
| "default_timeout_seconds": 120,
|
| "backends": {},
|
| }
|
|
|
|
|
| def default_gateway_config_path() -> Path:
|
| candidates = [
|
| Path.cwd() / "products" / "lumynax-marama-route" / "configs" / "gateway.local.json",
|
| Path.cwd() / "configs" / "gateway.local.json",
|
| PACKAGE_ROOT / "configs" / "gateway.local.json",
|
| PACKAGE_PARENT / "configs" / "gateway.local.json",
|
| ]
|
| for path in candidates:
|
| if path.exists():
|
| return path
|
| return candidates[0]
|
|
|
|
|
| def default_route_request_path() -> Path:
|
| candidates = [
|
| Path.cwd() / "products" / "lumynax-marama-route" / "examples" / "request.code-restricted.json",
|
| Path.cwd() / "examples" / "request.code-restricted.json",
|
| PACKAGE_ROOT / "examples" / "request.code-restricted.json",
|
| PACKAGE_PARENT / "examples" / "request.code-restricted.json",
|
| ]
|
| for path in candidates:
|
| if path.exists():
|
| return path
|
| return candidates[0]
|
|
|
|
|
| def load_gateway_config(path: Path | None = None) -> dict[str, Any]:
|
| config = dict(DEFAULT_GATEWAY_CONFIG)
|
| config["backends"] = dict(DEFAULT_GATEWAY_CONFIG["backends"])
|
| resolved = path or default_gateway_config_path()
|
| if resolved.exists():
|
| payload = json.loads(resolved.read_text(encoding="utf-8-sig"))
|
| if not isinstance(payload, dict):
|
| raise ValueError(f"Expected gateway config object in {resolved}")
|
| config.update(payload)
|
| config["backends"] = dict(payload.get("backends") or {})
|
| config["config_path"] = str(resolved)
|
| return config
|
|
|
|
|
| def handle_gateway_request(
|
| method: str,
|
| path: str,
|
| payload: dict[str, Any] | None,
|
| registry_path: Path,
|
| config_path: Path | None = None,
|
| ) -> tuple[int, dict[str, Any]]:
|
| models = load_model_registry(registry_path)
|
| config = load_gateway_config(config_path)
|
|
|
| if path.startswith("/api/"):
|
| return handle_api_request(method, path, payload, registry_path)
|
| if method == "GET" and path in {"/health", "/v1/health"}:
|
| return 200, {
|
| "ok": True,
|
| "product": PRODUCT_NAME,
|
| "mode": config["mode"],
|
| "model_count": len(models),
|
| "configured_backends": len(config.get("backends") or {}),
|
| "prompt_retention": config.get("prompt_retention", "not_stored_by_default"),
|
| }
|
| if method == "GET" and path == "/v1/models":
|
| return 200, build_models_api(models)
|
| if method == "POST" and path == "/v1/route" and payload is not None:
|
| result = route_or_chat_payload(payload, models)
|
| return (200 if result["ok"] else 422), result
|
| if method == "POST" and path == "/v1/chat/completions" and payload is not None:
|
| return chat_completion_gateway(payload, models, config)
|
| return 404, {"ok": False, "error": "not_found"}
|
|
|
|
|
| def chat_completion_gateway(
|
| payload: dict[str, Any],
|
| models: tuple[Any, ...],
|
| config: dict[str, Any],
|
| ) -> tuple[int, dict[str, Any]]:
|
| route_result = route_chat_payload(payload, models)
|
| decision = route_result["route_decision"]
|
| selected = decision.get("selected_model")
|
| if not isinstance(selected, dict):
|
| return 422, {"ok": False, "error": "no_eligible_model", **route_result}
|
|
|
| receipt = route_receipt(payload, route_result)
|
| dry_run = bool(
|
| payload.get("dry_run")
|
| or payload.get("marama_route_dry_run")
|
| or config.get("mode", "route_only") == "route_only"
|
| )
|
| if dry_run:
|
| response = dict(route_result["chat_completion_response"])
|
| response["marama_route"] = dict(response["marama_route"])
|
| response["marama_route"].update(
|
| {
|
| "backend_mode": "route_only",
|
| "receipt": receipt,
|
| "prompt_retention": config.get("prompt_retention", "not_stored_by_default"),
|
| },
|
| )
|
| return 200, response
|
|
|
| backend = _backend_for_model(selected["model_id"], config)
|
| if backend is None:
|
| return 424, {
|
| "ok": False,
|
| "error": "backend_not_configured",
|
| "message": "Routing succeeded, but no live backend is configured for the selected model.",
|
| "selected_model": selected["model_id"],
|
| "required_config": {
|
| "mode": "live",
|
| "backends": {
|
| selected["model_id"]: {
|
| "type": "openai_compatible",
|
| "base_url": "http://127.0.0.1:8000/v1",
|
| "api_key_env": "OPTIONAL_ENV_NAME",
|
| },
|
| },
|
| },
|
| "receipt": receipt,
|
| **route_result,
|
| }
|
| return _proxy_openai_chat_completion(payload, selected, backend, config, route_result, receipt)
|
|
|
|
|
| def smoke_gateway(
|
| *,
|
| registry_path: Path | None = None,
|
| config_path: Path | None = None,
|
| ) -> dict[str, Any]:
|
| resolved_registry = registry_path or default_registry_path()
|
| resolved_config = config_path or _temporary_route_only_config()
|
| route_payload = load_json_mapping(default_route_request_path())
|
| chat_payload = load_json_mapping(default_openai_chat_request_path())
|
| chat_payload["dry_run"] = True
|
|
|
| health_status, health = handle_gateway_request("GET", "/health", None, resolved_registry, resolved_config)
|
| models_status, models = handle_gateway_request("GET", "/v1/models", None, resolved_registry, resolved_config)
|
| route_status, route = handle_gateway_request("POST", "/v1/route", route_payload, resolved_registry, resolved_config)
|
| chat_status, chat = handle_gateway_request(
|
| "POST",
|
| "/v1/chat/completions",
|
| chat_payload,
|
| resolved_registry,
|
| resolved_config,
|
| )
|
|
|
| if health_status != 200 or models_status != 200 or route_status != 200 or chat_status != 200:
|
| raise RuntimeError("MaramaRoute gateway smoke failed")
|
| if chat.get("object") != "chat.completion" or chat["marama_route"]["selected_model"] is None:
|
| raise RuntimeError("MaramaRoute gateway did not return a routed chat response")
|
| return {
|
| "ok": True,
|
| "product": PRODUCT_NAME,
|
| "mode": health["mode"],
|
| "model_count": health["model_count"],
|
| "route_selected_model": route["route_decision"]["selected_model"]["model_id"],
|
| "chat_selected_model": chat["marama_route"]["selected_model"]["model_id"],
|
| "configured_backends": health["configured_backends"],
|
| }
|
|
|
|
|
| def serve_gateway(
|
| *,
|
| registry_path: Path | None = None,
|
| config_path: Path | None = None,
|
| host: str = "127.0.0.1",
|
| port: int = 8787,
|
| open_browser: bool = False,
|
| smoke: bool = False,
|
| ) -> int:
|
| resolved_registry = registry_path or default_registry_path()
|
| if smoke:
|
| print(json.dumps(smoke_gateway(registry_path=resolved_registry, config_path=config_path), indent=2, sort_keys=True))
|
| return 0
|
|
|
| html = build_expanded_dashboard_html(build_dashboard_state(resolved_registry))
|
| return serve_dashboard(
|
| product_name=f"{PRODUCT_NAME} Gateway",
|
| html=html,
|
| api_handler=lambda method, path, request_payload: handle_gateway_request(
|
| method,
|
| path,
|
| request_payload,
|
| resolved_registry,
|
| config_path,
|
| ),
|
| host=host,
|
| port=port,
|
| open_browser=open_browser,
|
| api_path_prefixes=("/api/", "/v1/"),
|
| api_exact_paths=("/health",),
|
| )
|
|
|
|
|
| def _backend_for_model(model_id: str, config: dict[str, Any]) -> dict[str, Any] | None:
|
| backends = config.get("backends")
|
| if not isinstance(backends, dict):
|
| return None
|
| backend = backends.get(model_id) or backends.get("*")
|
| return dict(backend) if isinstance(backend, dict) else None
|
|
|
|
|
| def _proxy_openai_chat_completion(
|
| payload: dict[str, Any],
|
| selected: dict[str, Any],
|
| backend: dict[str, Any],
|
| config: dict[str, Any],
|
| route_result: dict[str, Any],
|
| receipt: dict[str, Any],
|
| ) -> tuple[int, dict[str, Any]]:
|
| if str(backend.get("type") or "openai_compatible") != "openai_compatible":
|
| return 424, {"ok": False, "error": "unsupported_backend_type", "backend": backend}
|
|
|
| base_url = str(backend.get("base_url") or "").rstrip("/")
|
| if not base_url:
|
| return 424, {"ok": False, "error": "backend_base_url_missing", "selected_model": selected["model_id"]}
|
| endpoint = f"{base_url}/chat/completions"
|
| upstream_payload = dict(payload)
|
| upstream_payload["model"] = str(backend.get("model") or selected["model_id"])
|
| for key in ("route", "routing", "dry_run", "marama_route_dry_run"):
|
| upstream_payload.pop(key, None)
|
|
|
| headers = {"Content-Type": "application/json"}
|
| api_key_env = str(backend.get("api_key_env") or "")
|
| if api_key_env and os.getenv(api_key_env):
|
| headers["Authorization"] = f"Bearer {os.environ[api_key_env]}"
|
| headers.update({str(key): str(value) for key, value in dict(backend.get("headers") or {}).items()})
|
|
|
| timeout = float(backend.get("timeout_seconds") or config.get("default_timeout_seconds") or 120)
|
| request = urllib.request.Request(
|
| endpoint,
|
| data=json.dumps(upstream_payload).encode("utf-8"),
|
| headers=headers,
|
| method="POST",
|
| )
|
| try:
|
| with urllib.request.urlopen(request, timeout=timeout) as response:
|
| body = response.read().decode("utf-8")
|
| payload_out = json.loads(body)
|
| if not isinstance(payload_out, dict):
|
| raise ValueError("upstream response was not a JSON object")
|
| payload_out["marama_route"] = {
|
| "dry_run": False,
|
| "selected_model": selected,
|
| "fallback_models": route_result["route_decision"]["fallback_models"],
|
| "rejected_count": len(route_result["route_decision"]["rejected"]),
|
| "receipt": receipt,
|
| "backend_base_url": base_url,
|
| "prompt_retention": config.get("prompt_retention", "not_stored_by_default"),
|
| }
|
| return int(response.status), payload_out
|
| except urllib.error.HTTPError as exc:
|
| return exc.code, {
|
| "ok": False,
|
| "error": "backend_http_error",
|
| "status": exc.code,
|
| "body": exc.read().decode("utf-8", errors="replace"),
|
| "receipt": receipt,
|
| **route_result,
|
| }
|
| except Exception as exc:
|
| return 502, {
|
| "ok": False,
|
| "error": "backend_unavailable",
|
| "message": str(exc),
|
| "receipt": receipt,
|
| **route_result,
|
| }
|
|
|
|
|
| def _temporary_route_only_config() -> Path:
|
| path = Path(tempfile.gettempdir()) / "marama-route-smoke.gateway.json"
|
| path.write_text(json.dumps(DEFAULT_GATEWAY_CONFIG, indent=2, sort_keys=True), encoding="utf-8")
|
| return path
|
|
|