feat: add provider and model routing tables, admin UI, and audit category for gateway

Implement the multi-provider routing system for the OpenAI gateway, including two new database tables (`gateway_providers`, `gateway_models`) ensured at init, a new admin page at `/admin/gateway` with full CRUD for providers and model routes, and a `"gateway"` audit category mapped to `"ai"`. The routing layer sits transparently on top of the existing single-provider default path: unmatched model names fall through unchanged, while matched routes forward to the configured provider with their own pricing economy, vision model, and context window. Cross-worker cache invalidation uses a shared `_ROUTING_CACHE` bumped via `"gateway_routing"` cache version.
This commit is contained in:
2026-06-16 22:11:14 +00:00
parent c100b4b692
commit 7bc67662fa
21 changed files with 1401 additions and 20 deletions
@@ -112,6 +112,47 @@ def test_model_is_forced(local_db, monkeypatch):
assert rt._client.calls[-1][1]["model"] == "deepseek-chat"
def test_model_route_overrides_upstream(local_db, monkeypatch):
from devplacepy.services.openai_gateway import routing
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeClient_openai_gateway)
routing.provider_store.set(
routing.ProviderIn(
name="gwroute",
base_url="https://routed.example/v1/chat/completions",
api_key="routed-key",
)
)
routing.model_store.set(
routing.ModelRouteIn(
source_model="custom-pro",
provider="gwroute",
target_model="vendor/pro",
kind="chat",
price_output_per_m=9.0,
)
)
try:
svc = GatewayService()
cfg = svc.effective_config()
cfg["gateway_vision_enabled"] = False
rt = svc.runtime()
run_async(
rt.handle_chat(
{"model": "custom-pro", "messages": [{"role": "user", "content": "hi"}]},
cfg,
("guest", "test"),
"test",
)
)
url, body = rt._client.calls[-1]
assert str(url) == "https://routed.example/v1/chat/completions"
assert body["model"] == "vendor/pro"
finally:
routing.model_store.remove("custom-pro")
routing.provider_store.remove("gwroute")
def test_vision_rewrites_image_to_text(local_db, monkeypatch):
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeClient_openai_gateway)
svc = GatewayService()