fix(admin): resolve vendor/model-shaped ids in the availability routes #42

Merged
alee merged 1 commits from fix/admin-model-id-slash-routing into main 2026-09-06 21:04:51 +00:00
2 changed files with 64 additions and 3 deletions

View File

@@ -1392,7 +1392,7 @@ def build_router(
finally:
conn.close()
@router.get("/api/models/{model_id}/{provider}")
@router.get("/api/models/{model_id:path}/{provider}")
def admin_model_detail(model_id: str, provider: str) -> dict:
"""A single model's detail (same shape as one /api/models element)."""
conn = _db_callable()
@@ -1404,7 +1404,7 @@ def build_router(
finally:
conn.close()
@router.post("/api/models/{model_id}/{provider}/availability")
@router.post("/api/models/{model_id:path}/{provider}/availability")
def admin_set_availability(
model_id: str,
provider: str,
@@ -1502,7 +1502,7 @@ def build_router(
finally:
conn.close()
@router.delete("/api/models/{model_id}/{provider}/availability")
@router.delete("/api/models/{model_id:path}/{provider}/availability")
def admin_delete_availability(model_id: str, provider: str) -> dict:
"""Delete an admin availability override for a model.

View File

@@ -301,6 +301,67 @@ def test_delete_override_reverts_to_db_value(seeded_client):
assert data["availability"] == "active"
# --- vendor/model-shaped ids (OpenRouter's native id format) ---------------
#
# Every OpenRouter model id contains a slash (e.g. "google/lyria-3-pro-preview"),
# and the browser dutifully encodeURIComponent()s it -- but Starlette decodes
# %2F back into a literal '/' before matching a plain `str` path parameter
# against the route, so the request splits into extra path segments and 404s.
# Confirmed live: every admin availability-override click on an OpenRouter
# model failed this way until model_id was declared `:path`.
@pytest.fixture
def slash_id_client(tmp_path, monkeypatch):
"""A TestClient seeded with one vendor/model-shaped id."""
conn = _make_db(tmp_path)
_seed_models(conn, ("google/lyria-3-pro-preview",))
conn.close()
monkeypatch.setattr(dispatcher.cfg.database, "path", str(tmp_path / "test.db"))
monkeypatch.setattr(dispatcher.cfg.verification, "local_llm_enabled", False)
monkeypatch.setattr(dispatcher.cfg.routing, "require_vision", False)
monkeypatch.setenv("NEURALWATT_API_KEY", "test-key")
with TestClient(dispatcher.app) as client:
yield client
def test_model_detail_resolves_slash_containing_model_id(slash_id_client):
"""GET /admin/api/models/{model_id}/{provider} works for a vendor/model id,
sent the way a browser actually sends it: %2F for the embedded slash."""
resp = slash_id_client.get(
"/admin/api/models/google%2Flyria-3-pro-preview/neuralwatt"
)
assert resp.status_code == 200
assert resp.json()["model_id"] == "google/lyria-3-pro-preview"
def test_post_override_resolves_slash_containing_model_id(slash_id_client):
"""POST .../availability works for a vendor/model id (%2F-encoded)."""
resp = slash_id_client.post(
"/admin/api/models/google%2Flyria-3-pro-preview/neuralwatt/availability",
json={"availability": "deprecated", "reason": "non-chat model"},
)
assert resp.status_code == 200
data = resp.json()
assert data["model_id"] == "google/lyria-3-pro-preview"
assert data["effective_availability"] == "deprecated"
def test_delete_override_resolves_slash_containing_model_id(slash_id_client):
"""DELETE .../availability works for a vendor/model id (%2F-encoded)."""
slash_id_client.post(
"/admin/api/models/google%2Flyria-3-pro-preview/neuralwatt/availability",
json={"availability": "deprecated", "reason": "test"},
)
resp = slash_id_client.delete(
"/admin/api/models/google%2Flyria-3-pro-preview/neuralwatt/availability"
)
assert resp.status_code == 200
assert resp.json()["effective_availability"] == "active"
def test_model_list_reflects_effective_availability(seeded_client):
"""GET /admin/api/models includes effective_availability and is_overridden."""
seeded_client.post(