fix: reject custom provider alias conflicts

maintainer edit: reject arbitrary custom provider keys that normalize to built-in provider names so runtime and WebUI settings cannot disagree about whether a provider is dynamic or built in.
This commit is contained in:
chengyongru 2026-06-11 22:21:44 +08:00 committed by Xubin Ren
parent b2d00a4ce0
commit af9f9ebfd7
3 changed files with 20 additions and 1 deletions

View File

@ -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`. 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`.

View File

@ -245,7 +245,14 @@ class ProvidersConfig(Base):
def convert_extra_providers(self): def convert_extra_providers(self):
"""Convert extra fields (custom providers) to ProviderConfig objects.""" """Convert extra fields (custom providers) to ProviderConfig objects."""
if self.model_extra: if self.model_extra:
from nanobot.providers.registry import find_by_name
for key, value in self.model_extra.items(): 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): if isinstance(value, dict):
self.model_extra[key] = ProviderConfig.model_validate(value) self.model_extra[key] = ProviderConfig.model_validate(value)
return self return self

View File

@ -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: def test_custom_provider_fallback_uses_model_extra_without_pydantic_warnings() -> None:
config = Config.model_validate({ config = Config.model_validate({
"agents": { "agents": {