diff --git a/docs/providers.md b/docs/providers.md index 02760622a..75adc65a5 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -234,7 +234,7 @@ If you have more than one custom OpenAI-compatible endpoint, give each endpoint } ``` -Custom provider keys are treated as direct OpenAI-compatible providers. `apiBase` is required because nanobot cannot know the endpoint URL. `apiKey` is optional for local servers or private proxies that do not require one. Do not set `apiType` on custom provider keys; `apiType` is only for `providers.openai`. +Custom provider keys are treated as direct OpenAI-compatible providers. `apiBase` is required because nanobot cannot know the endpoint URL. `apiKey` is optional for local servers or private proxies that do not require one. Choose a name that does not conflict with a built-in provider name or alias, such as `openai`, `openai-codex`, `github-copilot`, or `lm-studio`. Do not set `apiType` on custom provider keys; `apiType` is only for `providers.openai`. This named custom provider path is not for Anthropic-compatible endpoints. For Anthropic-compatible proxies, use `providers.anthropic.apiBase` and set the preset provider to `anthropic`. diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 2d4079dd2..4e2a4599d 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -245,7 +245,14 @@ class ProvidersConfig(Base): def convert_extra_providers(self): """Convert extra fields (custom providers) to ProviderConfig objects.""" if self.model_extra: + from nanobot.providers.registry import find_by_name + for key, value in self.model_extra.items(): + if spec := find_by_name(key): + raise ValueError( + f"providers.{key} conflicts with built-in provider {spec.name!r}; " + "use the built-in provider key or choose a different custom provider name" + ) if isinstance(value, dict): self.model_extra[key] = ProviderConfig.model_validate(value) return self diff --git a/tests/config/test_model_presets.py b/tests/config/test_model_presets.py index a66014bb9..9519e1e81 100644 --- a/tests/config/test_model_presets.py +++ b/tests/config/test_model_presets.py @@ -60,6 +60,18 @@ def test_provider_api_type_is_openai_only() -> None: }) +@pytest.mark.parametrize("provider_name", ["openai-codex", "github-copilot", "lm-studio"]) +def test_dynamic_custom_provider_rejects_builtin_provider_aliases(provider_name: str) -> None: + with pytest.raises(ValueError, match="conflicts with built-in provider"): + Config.model_validate({ + "providers": { + provider_name: { + "apiBase": "https://example.test/v1", + } + } + }) + + def test_custom_provider_fallback_uses_model_extra_without_pydantic_warnings() -> None: config = Config.model_validate({ "agents": {