mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-15 07:14:08 +00:00
fix: strip dynamic custom provider route prefixes
maintainer edit: preserve provider-prefix CLI routing for named custom providers by stripping only the matched dynamic route prefix before sending the model id to OpenAI-compatible endpoints. This keeps ordinary namespaced model ids intact when the provider is selected explicitly.
This commit is contained in:
parent
09d24e6c25
commit
b2d00a4ce0
@ -18,6 +18,7 @@ from typing import TYPE_CHECKING, Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from loguru import logger
|
||||
from pydantic.alias_generators import to_snake
|
||||
|
||||
from nanobot.providers.base import (
|
||||
LLMProvider,
|
||||
@ -93,6 +94,10 @@ def _model_slug(model_name: str) -> str:
|
||||
return model_name.lower().rsplit("/", 1)[-1]
|
||||
|
||||
|
||||
def _provider_prefix_key(name: str) -> str:
|
||||
return to_snake(name.replace("-", "_")).lower()
|
||||
|
||||
|
||||
def _requires_max_completion_tokens(model_name: str) -> bool:
|
||||
"""Return True for models that reject ``max_tokens`` (GPT-5 family, o-series)."""
|
||||
slug = _model_slug(model_name)
|
||||
@ -592,6 +597,22 @@ class OpenAICompatProvider(LLMProvider):
|
||||
# Build kwargs
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _request_model_name(self, model_name: str) -> str:
|
||||
spec = self._spec
|
||||
if not spec or "/" not in model_name:
|
||||
return model_name
|
||||
if spec.strip_model_prefix:
|
||||
return model_name.split("/")[-1]
|
||||
|
||||
route_prefixes = getattr(spec, "strip_model_prefixes", ())
|
||||
if not isinstance(route_prefixes, tuple) or not route_prefixes:
|
||||
return model_name
|
||||
model_prefix, routed_model = model_name.split("/", 1)
|
||||
model_prefix_key = _provider_prefix_key(model_prefix)
|
||||
if any(_provider_prefix_key(prefix) == model_prefix_key for prefix in route_prefixes):
|
||||
return routed_model
|
||||
return model_name
|
||||
|
||||
@staticmethod
|
||||
def _supports_temperature(
|
||||
model_name: str,
|
||||
@ -625,8 +646,7 @@ class OpenAICompatProvider(LLMProvider):
|
||||
if any(model_name.lower().startswith(k) for k in ("anthropic/", "claude")):
|
||||
messages, tools = self._apply_cache_control(messages, tools)
|
||||
|
||||
if spec and spec.strip_model_prefix:
|
||||
model_name = model_name.split("/")[-1]
|
||||
model_name = self._request_model_name(model_name)
|
||||
|
||||
kwargs: dict[str, Any] = {
|
||||
"model": model_name,
|
||||
@ -830,8 +850,7 @@ class OpenAICompatProvider(LLMProvider):
|
||||
) -> dict[str, Any]:
|
||||
"""Build a Responses API body for direct OpenAI requests."""
|
||||
model_name = model or self.default_model
|
||||
if self._spec and self._spec.strip_model_prefix:
|
||||
model_name = model_name.split("/")[-1]
|
||||
model_name = self._request_model_name(model_name)
|
||||
sanitized_messages = self._sanitize_messages(self._sanitize_empty_content(messages))
|
||||
instructions, input_items = convert_messages(sanitized_messages)
|
||||
|
||||
|
||||
@ -49,6 +49,7 @@ class ProviderSpec:
|
||||
|
||||
# gateway behavior
|
||||
strip_model_prefix: bool = False # strip "provider/" before sending to gateway
|
||||
strip_model_prefixes: tuple[str, ...] = () # strip only when the first model segment matches
|
||||
supports_max_completion_tokens: bool = False
|
||||
|
||||
# per-model param overrides, e.g. (("kimi-k2.5", {"temperature": 1.0}),)
|
||||
@ -550,6 +551,7 @@ def find_by_name(name: str) -> ProviderSpec | None:
|
||||
def create_dynamic_spec(name: str) -> ProviderSpec:
|
||||
"""Create a dynamic ProviderSpec for custom user-defined providers."""
|
||||
normalized = to_snake(name.replace("-", "_"))
|
||||
strip_prefixes = tuple(dict.fromkeys((name, normalized)))
|
||||
return ProviderSpec(
|
||||
name=normalized,
|
||||
keywords=(),
|
||||
@ -557,4 +559,5 @@ def create_dynamic_spec(name: str) -> ProviderSpec:
|
||||
display_name=name.title(),
|
||||
backend="openai_compat",
|
||||
is_direct=True,
|
||||
strip_model_prefixes=strip_prefixes,
|
||||
)
|
||||
|
||||
@ -688,6 +688,106 @@ def test_make_provider_treats_dynamic_custom_provider_as_direct():
|
||||
assert kwargs["base_url"] == "https://example.com/v1"
|
||||
|
||||
|
||||
def test_make_provider_strips_dynamic_custom_route_prefix_from_request_model():
|
||||
config = Config.model_validate(
|
||||
{
|
||||
"agents": {"defaults": {"provider": "auto", "model": "my-company-api/gpt-4o-mini"}},
|
||||
"providers": {
|
||||
"my-company-api": {
|
||||
"apiBase": "https://example.com/v1",
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
provider = make_provider(config)
|
||||
|
||||
kwargs = provider._build_kwargs(
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
tools=None,
|
||||
model=None,
|
||||
max_tokens=16,
|
||||
temperature=0.1,
|
||||
reasoning_effort=None,
|
||||
tool_choice=None,
|
||||
)
|
||||
body = provider._build_responses_body(
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
tools=None,
|
||||
model=None,
|
||||
max_tokens=16,
|
||||
temperature=0.1,
|
||||
reasoning_effort=None,
|
||||
tool_choice=None,
|
||||
)
|
||||
|
||||
assert config.get_provider_name() == "my-company-api"
|
||||
assert kwargs["model"] == "gpt-4o-mini"
|
||||
assert body["model"] == "gpt-4o-mini"
|
||||
|
||||
|
||||
def test_make_provider_preserves_namespaced_model_for_forced_dynamic_provider():
|
||||
config = Config.model_validate(
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"provider": "my-company-api",
|
||||
"model": "openai/gpt-4o-mini",
|
||||
}
|
||||
},
|
||||
"providers": {
|
||||
"my-company-api": {
|
||||
"apiBase": "https://example.com/v1",
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
provider = make_provider(config)
|
||||
kwargs = provider._build_kwargs(
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
tools=None,
|
||||
model=None,
|
||||
max_tokens=16,
|
||||
temperature=0.1,
|
||||
reasoning_effort=None,
|
||||
tool_choice=None,
|
||||
)
|
||||
|
||||
assert kwargs["model"] == "openai/gpt-4o-mini"
|
||||
|
||||
|
||||
def test_make_provider_strips_dynamic_custom_route_prefix_once():
|
||||
config = Config.model_validate(
|
||||
{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"provider": "auto",
|
||||
"model": "my-company-api/openai/gpt-4o-mini",
|
||||
}
|
||||
},
|
||||
"providers": {
|
||||
"my-company-api": {
|
||||
"apiBase": "https://example.com/v1",
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
provider = make_provider(config)
|
||||
kwargs = provider._build_kwargs(
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
tools=None,
|
||||
model=None,
|
||||
max_tokens=16,
|
||||
temperature=0.1,
|
||||
reasoning_effort=None,
|
||||
tool_choice=None,
|
||||
)
|
||||
|
||||
assert kwargs["model"] == "openai/gpt-4o-mini"
|
||||
|
||||
|
||||
def test_make_provider_rejects_dynamic_custom_provider_without_api_base():
|
||||
config = Config.model_validate(
|
||||
{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user