alishahryar1--free-claude-code
a092455b54
## Problem Admin model routing fields required users to construct provider-prefixed model slugs. Optional tier overrides represented inheritance as an unexplained blank value. ## Changes | Before | After | | --- | --- | | Model inputs only gained suggestions after an individual provider refresh. | Model inputs load configured and discovered canonical slugs from one Admin catalog. | | Model routing looked like unrestricted text entry. | Model routing uses the browser's searchable model dropdown while retaining manual entry. | | Tier overrides displayed an empty value for fallback routing. | Tier overrides display **None** and persist it as an unset override. | | Model refresh returned provider-shaped cache internals. | Model refresh returns the same canonical catalog consumed by the Admin UI. | | Users inferred the provider/model slug format from examples. | The Admin UI and README define and present complete provider/model slugs. | <!-- greptile_comment --> <details open><summary><h3>Greptile Summary</h3></summary> This PR adds searchable model selection to the Admin UI. The main changes are: - Adds a canonical catalog of configured and discovered model slugs. - Adds searchable model inputs while preserving manual entry. - Represents unset tier overrides as **None**. - Reports provider-specific model refresh failures. - Reconciles cached models when provider settings change. - Updates documentation, package metadata, and tests. </details> <h3>Confidence Score: 5/5</h3> This looks safe to merge. Catalog failures no longer stop the rest of the Admin UI from loading. Partial provider refreshes now produce a visible warning. Removed credential-backed providers are pruned from the shared cache, and later stale writes are rejected. No blocking issues remain in the changed code. <details><summary><h3><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="20" align="absmiddle"></a> T-Rex Logs</h3></summary> **What T-Rex did** - T-Rex ran the requested verification for the pull request checks. - The verification completed, but local artifact references were not uploaded. <sub><a href="https://www.greptile.com/trex"><img alt="T-Rex" src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg" height="14" align="absmiddle"></a> Ran code and verified through T-Rex</sub> </details> <details open><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | src/free_claude_code/api/admin_static/admin.js | Adds searchable model fields, optional catalog hydration, refresh warnings, and None-to-unset conversion. | | src/free_claude_code/api/admin_routes.py | Adds canonical model catalog endpoints and provider refresh failure metadata. | | src/free_claude_code/providers/runtime/discovery.py | Tracks provider refresh outcomes and separates cache eligibility from discovery eligibility. | | src/free_claude_code/providers/runtime/model_cache.py | Scopes cached model metadata to currently available providers and removes stale remote entries. | | src/free_claude_code/runtime/provider_manager.py | Reconciles cache scope during runtime replacement and returns explicit refresh results. | </details> <sub>Reviews (2): Last reviewed commit: ["Fix model catalog refresh lifecycle"](https://github.com/alishahryar1/free-claude-code/commit/d16e170055f5389e538e18dece269a5f7a8c599d) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=44373795)</sub> <!-- /greptile_comment -->
151 行
5.1 KiB
Python
151 行
5.1 KiB
Python
from fastapi.testclient import TestClient
|
|
|
|
from free_claude_code.application.model_metadata import ProviderModelInfo
|
|
from free_claude_code.config.settings import Settings
|
|
from tests.api.support import create_test_app, provider_manager_for_app
|
|
|
|
|
|
def _settings(
|
|
*,
|
|
model: str = "deepseek/deepseek-chat",
|
|
model_fable: str | None = None,
|
|
model_opus: str | None = "open_router/anthropic/claude-opus",
|
|
model_haiku: str | None = "deepseek/deepseek-chat",
|
|
) -> Settings:
|
|
return Settings.model_construct(
|
|
model=model,
|
|
model_fable=model_fable,
|
|
model_opus=model_opus,
|
|
model_sonnet=None,
|
|
model_haiku=model_haiku,
|
|
anthropic_auth_token="",
|
|
deepseek_api_key="deepseek-key",
|
|
open_router_api_key="open-router-key",
|
|
wafer_api_key="wafer-key",
|
|
)
|
|
|
|
|
|
def _cache_models(app, provider_id: str, *model_ids: str) -> None:
|
|
provider_manager_for_app(app).cache_model_infos(
|
|
provider_id,
|
|
{ProviderModelInfo(model_id) for model_id in model_ids},
|
|
)
|
|
|
|
|
|
def test_models_list_includes_configured_refs_cached_provider_models_and_aliases():
|
|
app = create_test_app(_settings())
|
|
_cache_models(app, "deepseek", "deepseek-chat")
|
|
_cache_models(
|
|
app,
|
|
"open_router",
|
|
"meta/llama-3.3",
|
|
"anthropic/claude-opus",
|
|
)
|
|
|
|
response = TestClient(app).get("/v1/models")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
ids = [item["id"] for item in data["data"]]
|
|
assert ids[:6] == [
|
|
"anthropic/deepseek/deepseek-chat",
|
|
"claude-3-freecc-no-thinking/deepseek/deepseek-chat",
|
|
"anthropic/open_router/anthropic/claude-opus",
|
|
"claude-3-freecc-no-thinking/open_router/anthropic/claude-opus",
|
|
"anthropic/open_router/meta/llama-3.3",
|
|
"claude-3-freecc-no-thinking/open_router/meta/llama-3.3",
|
|
]
|
|
assert ids.count("anthropic/deepseek/deepseek-chat") == 1
|
|
assert ids.count("anthropic/open_router/anthropic/claude-opus") == 1
|
|
display_names = {item["id"]: item["display_name"] for item in data["data"]}
|
|
assert (
|
|
display_names["anthropic/open_router/meta/llama-3.3"]
|
|
== "open_router/meta/llama-3.3"
|
|
)
|
|
assert (
|
|
display_names["claude-3-freecc-no-thinking/open_router/meta/llama-3.3"]
|
|
== "open_router/meta/llama-3.3 (no thinking)"
|
|
)
|
|
assert "claude-sonnet-4-20250514" in ids
|
|
assert "claude-fable-5" in ids
|
|
assert data["first_id"] == ids[0]
|
|
assert data["last_id"] == ids[-1]
|
|
assert data["has_more"] is False
|
|
|
|
|
|
def test_models_list_uses_thinking_metadata_for_cached_models():
|
|
app = create_test_app(_settings(model_opus=None))
|
|
manager = provider_manager_for_app(app)
|
|
_cache_models(app, "deepseek", "deepseek-chat")
|
|
manager.cache_model_infos(
|
|
"open_router",
|
|
{
|
|
ProviderModelInfo("reasoning-model", supports_thinking=True),
|
|
ProviderModelInfo("plain-model", supports_thinking=False),
|
|
},
|
|
)
|
|
|
|
response = TestClient(app).get("/v1/models")
|
|
|
|
assert response.status_code == 200
|
|
ids = [item["id"] for item in response.json()["data"]]
|
|
assert "anthropic/open_router/reasoning-model" in ids
|
|
assert "claude-3-freecc-no-thinking/open_router/reasoning-model" in ids
|
|
assert "anthropic/open_router/plain-model" not in ids
|
|
assert "claude-3-freecc-no-thinking/open_router/plain-model" in ids
|
|
|
|
|
|
def test_models_list_uses_cached_metadata_for_configured_refs():
|
|
app = create_test_app(
|
|
_settings(
|
|
model="open_router/plain-model",
|
|
model_opus=None,
|
|
model_haiku=None,
|
|
)
|
|
)
|
|
provider_manager_for_app(app).cache_model_infos(
|
|
"open_router",
|
|
{ProviderModelInfo("plain-model", supports_thinking=False)},
|
|
)
|
|
|
|
response = TestClient(app).get("/v1/models")
|
|
|
|
ids = [item["id"] for item in response.json()["data"]]
|
|
assert "anthropic/open_router/plain-model" not in ids
|
|
assert ids[0] == "claude-3-freecc-no-thinking/open_router/plain-model"
|
|
|
|
|
|
def test_models_list_includes_cached_wafer_models():
|
|
app = create_test_app(
|
|
_settings(
|
|
model="wafer/DeepSeek-V4-Pro",
|
|
model_opus=None,
|
|
model_haiku=None,
|
|
)
|
|
)
|
|
_cache_models(app, "wafer", "DeepSeek-V4-Pro", "MiniMax-M2.7")
|
|
|
|
response = TestClient(app).get("/v1/models")
|
|
|
|
ids = [item["id"] for item in response.json()["data"]]
|
|
assert "anthropic/wafer/DeepSeek-V4-Pro" in ids
|
|
assert "claude-3-freecc-no-thinking/wafer/DeepSeek-V4-Pro" in ids
|
|
assert "anthropic/wafer/MiniMax-M2.7" in ids
|
|
assert "claude-3-freecc-no-thinking/wafer/MiniMax-M2.7" in ids
|
|
|
|
|
|
def test_models_list_works_with_empty_discovery_catalog():
|
|
app = create_test_app(_settings())
|
|
|
|
response = TestClient(app).get("/v1/models")
|
|
|
|
assert response.status_code == 200
|
|
ids = [item["id"] for item in response.json()["data"]]
|
|
assert ids[:4] == [
|
|
"anthropic/deepseek/deepseek-chat",
|
|
"claude-3-freecc-no-thinking/deepseek/deepseek-chat",
|
|
"anthropic/open_router/anthropic/claude-opus",
|
|
"claude-3-freecc-no-thinking/open_router/anthropic/claude-opus",
|
|
]
|
|
assert "claude-sonnet-4-20250514" in ids
|