feat(2.0): Phase 6c — eingebauter OpenAI-Gateway (model:auto) statt LiteLLM

LiteLLM baut auf Python 3.14 nicht (orjson-Pin ohne cp314-Wheel). Stattdessen
eingebauter Gateway in MC2: routers/gateway_proxy.py (/v1/chat/completions,
/completions, /models) + services/router_logic.py (Komplexitaets-Routing
fast<->heavy, Streaming-Passthrough). gateway.py/routing.py/connect.py auf
builtin umgestellt (Endpunkt = MC :PORT/v1). Gleicher OpenAI-Vertrag,
spaeter gegen LiteLLM austauschbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-25 12:54:19 +02:00
parent db1f62227b
commit ceca2ae8e3
10 changed files with 143 additions and 99 deletions
+10 -10
View File
@@ -9,23 +9,24 @@ die häufigste Fehlerquelle. Der Aufrufer übergibt den Host explizit.
import json
from config import PORT
DEFAULT_HOST = "192.168.178.151"
GATEWAY_PORT = 4000
MC_PORT = 9000
# Modelle, die der Gateway anbietet (model:auto = Standard).
GATEWAY_MODELS = ["auto", "fast", "heavy", "coder", "vision"]
def _gw(host: str) -> str:
return f"http://{host}:{GATEWAY_PORT}/v1"
# Eingebauter Gateway: MC2 serviert /v1 selbst (gleicher Port wie das Cockpit).
return f"http://{host}:{PORT}/v1"
def build_snippets(host: str = DEFAULT_HOST,
mcp_script_path: str = r"C:\\Users\\TobisPC\\mission-control-v2\\mcp\\mcp_memory.py",
mcp_python: str = "python") -> dict:
gw = _gw(host)
mc_url = f"http://{host}:{MC_PORT}"
mc_url = f"http://{host}:{PORT}"
cline = json.dumps({
"apiProvider": "openai",
@@ -66,13 +67,12 @@ def build_snippets(host: str = DEFAULT_HOST,
]
}, indent=2)
# Claude Code: Anthropic-Format → LiteLLM kann /v1/messages anbieten.
# Claude Code spricht Anthropic-Format; der eingebaute Gateway ist OpenAI-kompatibel.
claude_code = (
f'# Claude Code gegen den lokalen Gateway (Anthropic-kompatibel via LiteLLM):\n'
f'export ANTHROPIC_BASE_URL="http://{host}:{GATEWAY_PORT}"\n'
f'export ANTHROPIC_API_KEY="local"\n'
f'export ANTHROPIC_MODEL="auto"\n'
f'# (LiteLLM muss den Anthropic-/v1/messages-Endpunkt aktiviert haben — auf der Box prüfen.)'
f"# Der eingebaute Gateway ist OpenAI-kompatibel ({gw}, model: auto).\n"
f"# Claude Code nutzt das Anthropic-Format — dafür braucht es einen Anthropic-Shim\n"
f"# (z.B. LiteLLM /v1/messages) als Aufsatz. Für lokale Modelle direkt: Cline / OpenCode /\n"
f"# Continue / Zed nutzen (oben), die sprechen OpenAI-kompatibel mit diesem Gateway."
)
memory_mcp = json.dumps({
+19 -58
View File
@@ -1,69 +1,30 @@
"""
Routing-Gateway-Service: liest/schreibt die LiteLLM-Config und prüft die
Erreichbarkeit. MC verwaltet damit die Modell-Zuordnung (welcher llama-swap-Alias
ist fast/heavy/vision/coder) und die Routing-Regeln.
Routing-Gateway-Status (eingebauter Modus). MC2 IST der Gateway: serviert
`/v1/*` mit `model: auto`-Komplexitäts-Routing vor llama-swap. Kein externer
LiteLLM-Dienst nötig (baut auf Python 3.14 nicht); bleibt später austauschbar.
"""
import httpx
from config import GATEWAY_CONFIG_PATH, GATEWAY_URL, yaml
def read_gateway_config() -> dict:
if not GATEWAY_CONFIG_PATH.exists():
return {"model_list": [], "litellm_settings": {}, "router_settings": {}}
with GATEWAY_CONFIG_PATH.open("r", encoding="utf-8") as f:
return yaml.load(f) or {}
def write_gateway_config(cfg: dict) -> None:
import os
GATEWAY_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
tmp = GATEWAY_CONFIG_PATH.with_name(GATEWAY_CONFIG_PATH.name + ".tmp")
with tmp.open("w", encoding="utf-8") as f:
yaml.dump(cfg, f)
os.replace(tmp, GATEWAY_CONFIG_PATH)
from config import PORT
from services.llamaswap import engine_reachable
from services.router_logic import FAST, HEAVY, HEAVY_CHARS
def routing_summary() -> dict:
"""Kompakte Sicht für die UI: welcher Backend-Alias steckt hinter welchem
Gateway-Modellnamen + die Fallback-Ketten."""
cfg = read_gateway_config()
routes = []
for entry in cfg.get("model_list") or []:
params = entry.get("litellm_params") or {}
routes.append({
"name": entry.get("model_name"),
"target": str(params.get("model", "")),
"api_base": params.get("api_base"),
})
settings = cfg.get("litellm_settings") or {}
return {
"routes": routes,
"fallbacks": settings.get("fallbacks") or [],
"context_window_fallbacks": settings.get("context_window_fallbacks") or [],
"mode": "builtin",
"endpoint": f":{PORT}/v1 (OpenAI-kompatibel)",
"routes": [
{"name": "auto", "target": f"{FAST}{HEAVY} (nach Komplexität)"},
{"name": FAST, "target": "llama-swap-Alias 'fast'"},
{"name": HEAVY, "target": "llama-swap-Alias 'heavy'"},
{"name": "<alias>", "target": "llama-swap-Passthrough (lädt bei Bedarf)"},
],
"heavy_threshold_chars": HEAVY_CHARS,
"fallbacks": [],
"context_window_fallbacks": [],
}
def set_route(name: str, target_alias: str, api_base: str = "http://127.0.0.1:8080/v1") -> None:
"""Einen Gateway-Modellnamen (z.B. 'fast') auf einen llama-swap-Alias mappen."""
cfg = read_gateway_config()
ml = cfg.setdefault("model_list", [])
params = {"model": f"openai/{target_alias}", "api_base": api_base, "api_key": "sk-noauth"}
for entry in ml:
if entry.get("model_name") == name:
entry["litellm_params"] = params
break
else:
ml.append({"model_name": name, "litellm_params": params})
write_gateway_config(cfg)
def gateway_reachable() -> bool:
try:
with httpx.Client(timeout=3.0) as c:
# LiteLLM hat /health/liveliness; /v1/models tut's auch.
r = c.get(f"{GATEWAY_URL}/v1/models")
return r.status_code in (200, 401)
except Exception:
return False
# Der eingebaute Gateway lebt in MC und proxyt llama-swap → erreichbar, wenn Engine läuft.
return engine_reachable()
+30
View File
@@ -0,0 +1,30 @@
"""
Komplexitäts-Routing für `model: auto` (eingebauter Gateway).
Schnell im Alltag (fast), schwer bei Bedarf (heavy) — regelbasiert, sub-ms, ohne Cloud.
"""
import os
import re
FAST = os.environ.get("MC_ROUTE_FAST", "fast")
HEAVY = os.environ.get("MC_ROUTE_HEAVY", "heavy")
HEAVY_CHARS = int(os.environ.get("MC_GATEWAY_HEAVY_CHARS", "8000"))
_HEAVY_KW = re.compile(
r"\b(beweis|prove|theorem|refactor|architect|komplex|complex|schwierig|"
r"think\s*hard|reason\s*carefully|tief\s*nachdenk|optimi[sz]e|algorithm|"
r"root\s*cause|debug|analy[sz]e\s+deeply|step[-\s]?by[-\s]?step)\b",
re.IGNORECASE,
)
def choose_model(body: dict) -> tuple[str, str]:
"""Wählt fast|heavy für eine Chat-Anfrage. Gibt (alias, begründung) zurück."""
msgs = body.get("messages") or []
text = "\n".join(str(m.get("content") or "") for m in msgs)
n = len(text)
if n > HEAVY_CHARS:
return HEAVY, f"langer Kontext ({n} > {HEAVY_CHARS} Zeichen)"
if _HEAVY_KW.search(text):
return HEAVY, "Komplexitäts-Schlüsselwort erkannt"
return FAST, "Standard"