From 69fb382588add5d3805ab87a7643e8173cce1c4c Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sun, 6 Sep 2026 17:02:41 -0400 Subject: [PATCH] fix(admin): resolve vendor/model-shaped ids in the availability routes Every OpenRouter model id contains a slash (google/lyria-3-pro-preview), and the frontend already encodeURIComponent()s it before building the request URL -- but Starlette decodes %2F back into a literal '/' before matching a plain `str` path parameter, so the request splits into extra path segments and 404s. Declaring model_id as `:path` on all three routes (GET detail, POST/DELETE availability) fixes it the standard way. Confirmed live: every availability-override click on an OpenRouter model has been silently failing since #39 shipped, because nobody had exercised these routes against a slash-containing id until now. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01VRQXz5SYZYVWscxS1QqF6U --- src/admin.py | 6 ++-- tests/test_admin_models.py | 61 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/admin.py b/src/admin.py index 8a4b21a..70ed7eb 100644 --- a/src/admin.py +++ b/src/admin.py @@ -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. diff --git a/tests/test_admin_models.py b/tests/test_admin_models.py index 3acaed1..4d2f494 100644 --- a/tests/test_admin_models.py +++ b/tests/test_admin_models.py @@ -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( -- 2.49.1