Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,15 @@ class ProvidersConfig(_Base):

custom: ProviderConfig = Field(default_factory=ProviderConfig)
openrouter: ProviderConfig = Field(default_factory=ProviderConfig)
forge: ProviderConfig = Field(default_factory=ProviderConfig)
requesty: ProviderConfig = Field(default_factory=ProviderConfig)
anthropic: ProviderConfig = Field(default_factory=ProviderConfig)
openai: ProviderConfig = Field(default_factory=ProviderConfig)
deepseek: ProviderConfig = Field(default_factory=ProviderConfig)
gemini: ProviderConfig = Field(default_factory=ProviderConfig)
zhipu: ProviderConfig = Field(default_factory=ProviderConfig)
dashscope: ProviderConfig = Field(default_factory=ProviderConfig)
minimax: ProviderConfig = Field(default_factory=ProviderConfig)
vllm: ProviderConfig = Field(default_factory=ProviderConfig)
ollama: ProviderConfig = Field(default_factory=ProviderConfig)
profiles: dict[str, ConnectionProfileConfig] = Field(default_factory=dict)
Expand Down
23 changes: 22 additions & 1 deletion core/providers/openai_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@
"X-OpenRouter-Title": "DeepCode",
"X-OpenRouter-Categories": "cli-agent,research-agent",
}
_DEFAULT_REQUESTY_HEADERS = {
"HTTP-Referer": "https://github.com/HKUDS/DeepCode",
"X-Title": "DeepCode",
}
# Per-model thinking / reasoning quirks now live declaratively in
# ``core.providers.model_compat`` (resolved via ``resolve_model_compat``);
# this module only assembles requests from the resolved value.
Expand Down Expand Up @@ -158,6 +162,15 @@ def _uses_openrouter_attribution(
return bool(api_base and "openrouter" in api_base.lower())


def _uses_requesty_attribution(
spec: "ProviderSpec | None", api_base: str | None
) -> bool:
"""Apply DeepCode attribution headers to Requesty requests by default."""
if spec and spec.name == "requesty":
return True
return bool(api_base and "requesty" in api_base.lower())


_RESPONSES_FAILURE_THRESHOLD = 3
_RESPONSES_PROBE_INTERVAL_S = 300 # 5 minutes

Expand Down Expand Up @@ -232,6 +245,8 @@ def __init__(
default_headers = {"x-session-affinity": uuid.uuid4().hex}
if _uses_openrouter_attribution(spec, effective_base):
default_headers.update(_DEFAULT_OPENROUTER_HEADERS)
if _uses_requesty_attribution(spec, effective_base):
default_headers.update(_DEFAULT_REQUESTY_HEADERS)
if extra_headers:
default_headers.update(extra_headers)

Expand Down Expand Up @@ -262,7 +277,13 @@ def _setup_env(self, api_key: str, api_base: str | None) -> None:
resolved = env_val.replace("{api_key}", api_key).replace(
"{api_base}", effective_base
)
os.environ.setdefault(env_name, resolved)
# Same rule as ``spec.env_key`` above: a gateway routes to whatever
# the user selected, so its values must win over a stale ambient
# variable. Direct providers keep deferring to the environment.
if spec.is_gateway:
os.environ[env_name] = resolved
else:
os.environ.setdefault(env_name, resolved)

@classmethod
def _apply_cache_control(
Expand Down
32 changes: 32 additions & 0 deletions core/providers/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,30 @@ def label(self) -> str:
default_api_base="https://openrouter.ai/api/v1",
supports_prompt_caching=True,
),
ProviderSpec(
name="requesty",
keywords=("requesty",),
env_key="REQUESTY_API_KEY",
display_name="Requesty",
backend="openai_compat",
is_gateway=True,
detect_by_base_keyword="requesty",
default_api_base="https://router.requesty.ai/v1",
supports_prompt_caching=True,
),
ProviderSpec(
name="forge",
keywords=("forge",),
env_key="FORGE_API_KEY",
display_name="Forge",
backend="openai_compat",
is_gateway=True,
detect_by_base_keyword="forge.tensorblock.co",
default_api_base="https://api.forge.tensorblock.co/v1",
# Forge resolves bare model ids, not ``vendor/model`` — unlike the
# OpenRouter-style gateways above.
strip_model_prefix=True,
),
ProviderSpec(
name="anthropic",
keywords=("anthropic", "claude"),
Expand Down Expand Up @@ -134,6 +158,14 @@ def label(self) -> str:
default_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
thinking_style="enable_thinking",
),
ProviderSpec(
name="minimax",
keywords=("minimax", "abab"),
env_key="MINIMAX_API_KEY",
display_name="MiniMax",
backend="openai_compat",
default_api_base="https://api.minimax.io/v1",
),
ProviderSpec(
name="vllm",
keywords=("vllm",),
Expand Down
105 changes: 105 additions & 0 deletions tests/minimax_provider_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""Tests for MiniMax (minimax) provider integration."""

from __future__ import annotations

import sys
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))

from core.providers.registry import ( # noqa: E402
PROVIDERS,
find_by_model,
find_by_name,
)


# ---------------------------------------------------------------------------
# Core registry tests
# ---------------------------------------------------------------------------


class TestCoreRegistry:
"""Tests for the core provider registry."""

def test_find_by_name(self):
spec = find_by_name("minimax")
assert spec is not None
assert spec.name == "minimax"
assert spec.display_name == "MiniMax"

def test_env_key(self):
spec = find_by_name("minimax")
assert spec is not None
assert spec.env_key == "MINIMAX_API_KEY"

def test_backend_is_openai_compat(self):
spec = find_by_name("minimax")
assert spec is not None
assert spec.backend == "openai_compat"

def test_default_api_base(self):
spec = find_by_name("minimax")
assert spec is not None
assert spec.default_api_base == "https://api.minimax.io/v1"

def test_find_by_model_minimax(self):
spec = find_by_model("minimax/MiniMax-M3")
assert spec is not None
assert spec.name == "minimax"

def test_find_by_model_minimax_m27(self):
spec = find_by_model("minimax/MiniMax-M2.7")
assert spec is not None
assert spec.name == "minimax"

def test_find_by_model_abab(self):
spec = find_by_model("abab-7")
assert spec is not None
assert spec.name == "minimax"

def test_not_gateway_or_local(self):
spec = find_by_name("minimax")
assert spec is not None
assert spec.is_gateway is False
assert spec.is_local is False

def test_provider_in_registry_list(self):
names = [s.name for s in PROVIDERS]
assert "minimax" in names


# ---------------------------------------------------------------------------
# Config model tests
# ---------------------------------------------------------------------------


class TestConfigModel:
"""Tests for the config model with the new provider field."""

def test_providers_config_has_minimax_field(self):
from core.config import ProvidersConfig

cfg = ProvidersConfig()
assert hasattr(cfg, "minimax")
assert cfg.minimax.api_key is None

def test_providers_config_with_api_key(self):
from core.config import ProvidersConfig, ProviderConfig

cfg = ProvidersConfig(minimax=ProviderConfig(api_key="test-key"))
assert cfg.minimax.api_key == "test-key"

def test_providers_config_with_custom_base(self):
from core.config import ProvidersConfig, ProviderConfig

cfg = ProvidersConfig(
minimax=ProviderConfig(
api_key="test-key",
api_base="https://api.minimaxi.com/v1",
)
)
assert cfg.minimax.api_base == "https://api.minimaxi.com/v1"
75 changes: 75 additions & 0 deletions tests/test_forge_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Forge gateway registration, plus the gateway rule for ``env_extras``.

Ported from #116, which added Forge to the (since-removed) nanobot registry
and fixed the same ``setdefault`` bug in nanobot's provider. Core had that bug
half-fixed: ``spec.env_key`` was forced for gateways but ``env_extras`` was not.
"""

from __future__ import annotations

import os
import sys
from dataclasses import replace
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))

from core.config import ProvidersConfig # noqa: E402
from core.providers.openai_compat import OpenAICompatProvider # noqa: E402
from core.providers.registry import find_by_name # noqa: E402


def test_forge_is_registered_as_a_gateway():
spec = find_by_name("forge")
assert spec is not None
assert spec.is_gateway is True
assert spec.backend == "openai_compat"
assert spec.env_key == "FORGE_API_KEY"
assert spec.default_api_base == "https://api.forge.tensorblock.co/v1"


def test_forge_strips_the_vendor_prefix():
"""Unlike OpenRouter/Requesty, Forge resolves bare model ids."""

forge = find_by_name("forge")
openrouter = find_by_name("openrouter")
assert forge.strip_model_prefix is True
assert openrouter.strip_model_prefix is False


def test_forge_does_not_collide_with_other_gateway_detection():
spec = find_by_name("forge")
assert spec.detect_by_base_keyword == "forge.tensorblock.co"
assert spec.detect_by_key_prefix == ""


def test_providers_config_exposes_forge():
"""``config.py`` reads providers via ``getattr(..., spec.name)``, so a
missing field silently disables the provider everywhere."""

assert hasattr(ProvidersConfig(), "forge")


def test_gateway_env_extras_override_ambient_values(monkeypatch):
spec = replace(find_by_name("forge"), env_extras=(("FORGE_EXTRA", "{api_key}"),))
monkeypatch.setenv("FORGE_EXTRA", "stale-from-shell")
monkeypatch.delenv("FORGE_API_KEY", raising=False)

OpenAICompatProvider(api_key="fresh-key", spec=spec)._setup_env("fresh-key", None)

assert os.environ["FORGE_EXTRA"] == "fresh-key"


def test_direct_provider_env_extras_still_defer(monkeypatch):
"""Non-gateway providers keep deferring to the ambient environment."""

spec = replace(find_by_name("zhipu"), env_extras=(("ZHIPU_EXTRA", "{api_key}"),))
assert spec.is_gateway is False
monkeypatch.setenv("ZHIPU_EXTRA", "set-by-user")
monkeypatch.delenv(spec.env_key, raising=False)

OpenAICompatProvider(api_key="fresh-key", spec=spec)._setup_env("fresh-key", None)

assert os.environ["ZHIPU_EXTRA"] == "set-by-user"
72 changes: 72 additions & 0 deletions tests/test_requesty_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Pin the Requesty provider as a mirror of the OpenRouter provider.

The Requesty router is an OpenAI-compatible gateway wired on the same generic
``openai_compat`` path as OpenRouter. These tests assert that the registry
entry mirrors OpenRouter where it should (backend, gateway flag, prompt
caching, ``provider/model`` naming) while pinning the Requesty-specific base
URL / env var, and that the model-catalog normalizer maps Requesty's
capability shape (``context_window`` + ``supports_*`` booleans) onto the same
fields the settings UI already consumes for OpenRouter.
"""

from __future__ import annotations

import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
BACKEND = ROOT / "new_ui" / "backend"
if str(BACKEND) not in sys.path:
sys.path.insert(0, str(BACKEND))

from core.providers.registry import find_by_model, find_by_name # noqa: E402

REQUESTY = find_by_name("requesty")
OPENROUTER = find_by_name("openrouter")


# ---- registry --------------------------------------------------------------


def test_requesty_is_registered() -> None:
assert REQUESTY is not None
assert REQUESTY.name == "requesty"
assert REQUESTY.display_name == "Requesty"


def test_requesty_mirrors_openrouter_generic_wiring() -> None:
assert OPENROUTER is not None and REQUESTY is not None
# Same generic OpenAI-compatible gateway path as OpenRouter.
assert REQUESTY.backend == OPENROUTER.backend == "openai_compat"
assert REQUESTY.is_gateway is True
assert REQUESTY.supports_prompt_caching is True
assert REQUESTY.is_local is False
assert REQUESTY.is_oauth is False


def test_requesty_provider_specific_endpoint() -> None:
assert REQUESTY is not None
assert REQUESTY.default_api_base == "https://router.requesty.ai/v1"
assert REQUESTY.env_key == "REQUESTY_API_KEY"
assert REQUESTY.detect_by_base_keyword == "requesty"


def test_requesty_does_not_borrow_openrouter_key_prefix() -> None:
# OpenRouter keys start with ``sk-or-``; Requesty keys do not, so the
# prefix heuristic must not be copied over.
assert REQUESTY is not None
assert REQUESTY.detect_by_key_prefix == ""


def test_requesty_shares_provider_slash_model_naming() -> None:
# ``provider/model`` slugs resolve to the owning provider (openai/anthropic
# /...), exactly like OpenRouter -- Requesty adds no new namespace.
for model in ("openai/gpt-4o-mini", "anthropic/claude-sonnet-4-5"):
spec = find_by_model(model)
assert spec is not None
assert spec.name in {"openai", "anthropic"}


# ---- model catalog normalization ------------------------------------------
Loading