diff --git a/nanobot/providers/factory.py b/nanobot/providers/factory.py index 37fa2ed8b..c9346a137 100644 --- a/nanobot/providers/factory.py +++ b/nanobot/providers/factory.py @@ -49,6 +49,30 @@ def _provider_extra_headers( return headers or None +def _provider_spec_for_config( + provider_name: str, + provider_config: ProviderConfig | None, +) -> ProviderSpec | None: + spec = find_by_name(provider_name) + if ( + spec is not None + and spec.name == "orcarouter" + and provider_config is not None + and provider_config.api_base + and provider_config.api_base.rstrip("/").lower() + != spec.default_api_base.rstrip("/").lower() + ): + # Before OrcaRouter became a built-in provider, this name was valid for a + # dynamic custom provider. Preserve that provider's model-prefix behavior + # when an existing config points the name at a different endpoint. + return create_dynamic_spec( + provider_name, + display_name=provider_config.display_name or "", + thinking_style=provider_config.thinking_style or "", + ) + return spec + + def _resolve_provider_setup( config: Config, *, @@ -61,7 +85,7 @@ def _resolve_provider_setup( p = config.get_provider(model, preset=preset) if not provider_name: raise ValueError(f"No provider is configured for model '{model}'.") - spec = find_by_name(provider_name) + spec = _provider_spec_for_config(provider_name, p) if not spec and p: if not p.api_base: raise ValueError(f"Provider '{provider_name}' requires api_base in config.") diff --git a/tests/providers/test_orcarouter_provider.py b/tests/providers/test_orcarouter_provider.py index ebf9052ee..b5589bf57 100644 --- a/tests/providers/test_orcarouter_provider.py +++ b/tests/providers/test_orcarouter_provider.py @@ -3,6 +3,7 @@ from unittest.mock import patch from nanobot.config.schema import Config, ProvidersConfig +from nanobot.providers.factory import make_provider from nanobot.providers.openai_compat_provider import OpenAICompatProvider from nanobot.providers.registry import PROVIDERS, find_by_name @@ -55,7 +56,7 @@ def test_orcarouter_forced_provider_uses_default_api_base() -> None: assert config.get_api_base("deepseek/deepseek-chat") == "https://api.orcarouter.ai/v1" -def test_orcarouter_gateway_routes_unprefixed_models_when_configured() -> None: +def test_orcarouter_gateway_routes_auto_model_when_configured() -> None: config = Config.model_validate({ "providers": { "orcarouter": { @@ -74,6 +75,39 @@ def test_orcarouter_gateway_routes_unprefixed_models_when_configured() -> None: assert config.get_api_base("orcarouter/auto") == "https://api.orcarouter.ai/v1" +def test_legacy_custom_provider_named_orcarouter_keeps_prefix_stripping() -> None: + config = Config.model_validate({ + "providers": { + "orcarouter": { + "apiKey": "legacy-test-key", + "apiBase": "https://legacy-gateway.example/v1", + }, + }, + "agents": { + "defaults": { + "model": "orcarouter/custom-model", + "provider": "orcarouter", + }, + }, + }) + + with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"): + provider = make_provider(config) + + assert isinstance(provider, OpenAICompatProvider) + assert provider.api_base == "https://legacy-gateway.example/v1" + kwargs = provider._build_kwargs( + messages=[{"role": "user", "content": "hi"}], + tools=None, + model="orcarouter/custom-model", + max_tokens=1024, + temperature=0.7, + reasoning_effort=None, + tool_choice=None, + ) + assert kwargs["model"] == "custom-model" + + def test_orcarouter_preserves_model_api_id() -> None: spec = find_by_name("orcarouter") with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"):