diff --git a/tests/test_console_available_models.py b/tests/test_console_available_models.py index e754e74b..fbf7337d 100644 --- a/tests/test_console_available_models.py +++ b/tests/test_console_available_models.py @@ -372,6 +372,30 @@ def test_effort_ladder_parses_string_capabilities(storage: SQLiteBackend) -> Non assert ladder["max"] == "on" +def test_effort_ladder_key_survives_malformed_capabilities( + storage: SQLiteBackend, +) -> None: + """A capabilities column that fails to parse must not drop the key — + every row carries ``effort_ladder`` (empty on failure) so clients can + index it unconditionally instead of null-checking per row.""" + storage.create_model_definition( + definition_id="m1", + alias="broken", + model="model-x", + provider="openai-compatible", + base_url="http://localhost:8000/v1", + api_key="dummy", + context_window=131072, + capabilities="{not valid json", + enabled=True, + created_by="admin", + ) + body = _get_models(_make_client(storage)) + entry = body["models"][0] + assert set(entry) == {"alias", "model", "provider", "effort_ladder"} + assert entry["effort_ladder"] == [] + + def test_effort_ladder_honors_responses_api_surface(storage: SQLiteBackend) -> None: """server_compat.api_surface (namespaced inside the capabilities JSON) switches the projection to the flat-param path — no template toggle.""" diff --git a/turnstone/console/server.py b/turnstone/console/server.py index 0a201395..3d995f53 100644 --- a/turnstone/console/server.py +++ b/turnstone/console/server.py @@ -1916,10 +1916,14 @@ async def list_available_models(request: Request) -> JSONResponse: models = [] for r in rows: + # ``effort_ladder`` starts as the empty list so the row schema is + # stable even when the try block below bails on a malformed + # capabilities column — clients can index the key unconditionally. entry: dict[str, Any] = { "alias": r["alias"], "model": r["model"], "provider": r["provider"], + "effort_ladder": [], } try: # ``capabilities`` is a JSON string (sa.Text column) with