diff --git a/docs/configuration.md b/docs/configuration.md index ce3a293..138ef87 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -552,6 +552,7 @@ ccproxy: api.openai.com: openai generativelanguage.googleapis.com: google openrouter.ai: openrouter + router.requesty.ai: requesty mitmproxy: ssl_insecure: true web_host: 127.0.0.1 diff --git a/src/ccproxy/specs/model_catalog.py b/src/ccproxy/specs/model_catalog.py index ed48151..af1282f 100644 --- a/src/ccproxy/specs/model_catalog.py +++ b/src/ccproxy/specs/model_catalog.py @@ -52,6 +52,11 @@ def _perplexity_model_ids() -> list[str]: "deepseek": [ "deepseek-v4", ], + "requesty": [ + "openai/gpt-4o-mini", + "anthropic/claude-sonnet-4-5", + "google/gemini-2.5-flash", + ], "perplexity": _perplexity_model_ids(), } """Provider → model IDs floor list. Updated alongside provider releases.""" @@ -60,6 +65,7 @@ def _perplexity_model_ids() -> list[str]: _PROVIDER_ENDPOINTS: dict[str, str] = { "anthropic": "https://api.anthropic.com/v1/models", "openrouter": "https://openrouter.ai/api/v1/models", + "requesty": "https://router.requesty.ai/v1/models", } """Provider → upstream ``/v1/models`` URL for live merge. gemini is omitted because it requires GCP project context that ccproxy doesn't have at diff --git a/tests/test_model_catalog.py b/tests/test_model_catalog.py index c040aa0..2dd7d4d 100644 --- a/tests/test_model_catalog.py +++ b/tests/test_model_catalog.py @@ -43,6 +43,14 @@ def test_static_floor_contains_known_gemini_models() -> None: assert "gemini-2.5-flash" in ids +def test_static_floor_contains_known_requesty_models() -> None: + """The floor includes known Requesty (OpenAI-compatible gateway) model IDs.""" + catalog = build_catalog() + ids = {entry["id"] for entry in catalog["data"]} + assert "openai/gpt-4o-mini" in ids + assert "anthropic/claude-sonnet-4-5" in ids + + def test_owned_by_matches_provider_keys() -> None: """Each entry's ``owned_by`` is one of the provider keys in STATIC_MODEL_CATALOG.""" catalog = build_catalog() @@ -88,6 +96,33 @@ def handler(request: httpx.Request) -> httpx.Response: assert ids.count("claude-opus-4-7") == 1 +def test_refresh_merges_live_requesty_models() -> None: + """``refresh=True`` unions live Requesty models with the static floor (deduped).""" + set_config_instance(CCProxyConfig()) + + def handler(request: httpx.Request) -> httpx.Response: + if "router.requesty.ai" in str(request.url): + return httpx.Response( + 200, + json={ + "object": "list", + "data": [ + # one new model not in the floor + {"id": "deepseek/deepseek-chat", "object": "model", "created": 1700000000}, + # one duplicate of a floor entry + {"id": "openai/gpt-4o-mini", "object": "model"}, + ], + }, + ) + return httpx.Response(404) + + catalog = build_catalog(refresh=True, transport=httpx.MockTransport(handler)) + ids = [entry["id"] for entry in catalog["data"]] + assert "deepseek/deepseek-chat" in ids + # The duplicate floor entry is not double-counted (deduped by (owned_by, id)). + assert ids.count("openai/gpt-4o-mini") == 1 + + def test_refresh_provider_failure_falls_back_to_floor() -> None: """A provider HTTP failure does not remove its floor entries from the result.""" set_config_instance(CCProxyConfig())