From 267c4478eb0c142c92e09ac037ff01cb75c5896e Mon Sep 17 00:00:00 2001 From: dennyho0917 Date: Wed, 30 Sep 2026 14:13:22 +0800 Subject: [PATCH] feat: add API Route gateway provider --- core/config.py | 1 + core/providers/registry.py | 11 +++++++ docs/guide/models.md | 14 +++++++++ tests/test_api_route_provider.py | 53 ++++++++++++++++++++++++++++++++ tests/test_provider_egress.py | 1 + 5 files changed, 80 insertions(+) create mode 100644 tests/test_api_route_provider.py diff --git a/core/config.py b/core/config.py index d1ad421e8..9371ded96 100644 --- a/core/config.py +++ b/core/config.py @@ -318,6 +318,7 @@ class ProvidersConfig(_Base): forge: ProviderConfig = Field(default_factory=ProviderConfig) requesty: ProviderConfig = Field(default_factory=ProviderConfig) opper: ProviderConfig = Field(default_factory=ProviderConfig) + api_route: ProviderConfig = Field(default_factory=ProviderConfig) bedrock: ProviderConfig = Field(default_factory=ProviderConfig) anthropic: ProviderConfig = Field(default_factory=ProviderConfig) openai: ProviderConfig = Field(default_factory=ProviderConfig) diff --git a/core/providers/registry.py b/core/providers/registry.py index 61d084873..9a3eadf8b 100644 --- a/core/providers/registry.py +++ b/core/providers/registry.py @@ -236,6 +236,17 @@ def _matches_declared_endpoint(self, api_base: str | None) -> bool: # ``provider/model`` id like ``azure/gpt-5.5`` deliberately pins one # route, so the prefix must survive rather than be stripped. ), + ProviderSpec( + name="api_route", + keywords=("api_route", "api-route"), + env_key="API_ROUTE_API_KEY", + display_name="API Route", + backend="openai_compat", + is_gateway=True, + endpoint_class="gateway", + detect_by_base_keyword="api-route.com", + default_api_base="https://global.api-route.com/v1", + ), ProviderSpec( name="forge", keywords=("forge",), diff --git a/docs/guide/models.md b/docs/guide/models.md index 63447ca6a..3485d5726 100644 --- a/docs/guide/models.md +++ b/docs/guide/models.md @@ -32,6 +32,20 @@ deepcode provider test my-openrouter --model MODEL_ID This sends a short request to check that the model responds. Desktop/Web offer the same check through **Save and verify model**. +For API Route, choose **API Route** in Desktop/Web or add a connection from the +terminal. It uses the OpenAI-compatible endpoint at +`https://global.api-route.com/v1`: + +```console +deepcode provider set my-api-route --template api_route --api-key-env API_ROUTE_API_KEY +deepcode provider models my-api-route --refresh +deepcode provider test my-api-route --model MODEL_ID +``` + +Set `API_ROUTE_API_KEY` in your environment and replace `MODEL_ID` with an exact +ID returned by the catalog refresh. See the [API Route quickstart](https://www.api-route.com/docs/quickstart) +for key setup. + When trying a new model server, you can also check streaming and tool calls: ```console diff --git a/tests/test_api_route_provider.py b/tests/test_api_route_provider.py new file mode 100644 index 000000000..43d5caa46 --- /dev/null +++ b/tests/test_api_route_provider.py @@ -0,0 +1,53 @@ +"""API Route uses the shared OpenAI-compatible gateway path.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from core.config import ConfigError, DeepCodeConfig, make_llm_provider +from core.providers.openai_compat import OpenAICompatProvider +from core.providers.registry import find_by_name + + +def _config(api_key: str | None) -> DeepCodeConfig: + return DeepCodeConfig.model_validate( + { + "agents": {"defaults": {"provider": "api_route", "model": "gpt-5.5"}}, + "providers": {"api_route": {"apiKey": api_key}}, + } + ) + + +def test_api_route_is_a_named_gateway() -> None: + spec = find_by_name("api_route") + + assert spec is not None + assert spec.display_name == "API Route" + assert spec.env_key == "API_ROUTE_API_KEY" + assert spec.default_api_base == "https://global.api-route.com/v1" + assert spec.backend == "openai_compat" + assert spec.resolve_endpoint_class(spec.default_api_base) == "gateway" + assert spec.resolve_endpoint_class("https://other.example/v1") == "unknown" + assert spec.detect_by_key_prefix == "" + assert spec.strip_model_prefix is False + + +def test_api_route_connection_uses_its_key_and_default_endpoint() -> None: + provider = make_llm_provider(_config("test-api-route-key")) + + assert isinstance(provider, OpenAICompatProvider) + assert provider.api_key == "test-api-route-key" + assert provider.api_base == "https://global.api-route.com/v1" + assert provider.default_model == "gpt-5.5" + + +def test_api_route_requires_its_own_key() -> None: + with pytest.raises(ConfigError, match=r"providers\.api_route\.apiKey"): + make_llm_provider(_config(None)) diff --git a/tests/test_provider_egress.py b/tests/test_provider_egress.py index 0669e10b3..349167a22 100644 --- a/tests/test_provider_egress.py +++ b/tests/test_provider_egress.py @@ -201,6 +201,7 @@ def test_invalid_policy_entry_fails_closed(): ("openrouter", "gateway"), ("requesty", "gateway"), ("opper", "gateway"), + ("api_route", "gateway"), ("forge", "gateway"), ("zhipu", "aggregator"), ("dashscope", "aggregator"),