mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(websearch): keep the provider prefix on the agentic follow-up model
The agentic follow-up rebuilt its model string with "a slash means it is already qualified". Handlers are handed the provider-stripped model, and for a provider that routes through a sub-path the remainder still holds a slash, so bedrock/mantle/anthropic.claude-sonnet-5 arrived as mantle/anthropic.claude-sonnet-5 and stayed that way. The follow-up acompletion could not resolve a provider from it and raised "LLM Provider NOT provided". Prefix unless the model is already qualified with that same provider, in one shared helper used by both the handler and the websearch patch builder, which carried copies of the same heuristic. This also fixes openrouter, whose stripped model keeps a slash for the same reason, and the old startswith test that read openai as a prefix of openai_like. Fixes #38829
This commit is contained in:
parent
98d46ee59d
commit
0196c9da77
4 changed files with 61 additions and 8 deletions
|
|
@ -1794,11 +1794,9 @@ class WebSearchInterceptionLogger(CustomLogger):
|
|||
k: v for k, v in kwargs.items() if not k.startswith("_websearch_interception") and k not in internal_params
|
||||
}
|
||||
|
||||
full_model_name = model
|
||||
if "custom_llm_provider" in kwargs:
|
||||
custom_llm_provider: Final = kwargs["custom_llm_provider"]
|
||||
if not model.startswith(custom_llm_provider) and "/" not in model:
|
||||
full_model_name = f"{custom_llm_provider}/{model}"
|
||||
from litellm.litellm_core_utils.core_helpers import qualify_provider_stripped_model
|
||||
|
||||
full_model_name: Final = qualify_provider_stripped_model(model, kwargs.get("custom_llm_provider", ""))
|
||||
|
||||
verbose_logger.debug(
|
||||
"WebSearchInterception: Built chat completion request patch model=%s messages=%d",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,20 @@ def is_codex_user_agent(user_agent: str) -> bool:
|
|||
return bool(_CODEX_CLIENT_PREFIX_RE.match(user_agent))
|
||||
|
||||
|
||||
def qualify_provider_stripped_model(model: str, custom_llm_provider: str) -> str:
|
||||
"""
|
||||
Put the provider prefix back on a model an agentic follow-up re-dispatches with.
|
||||
|
||||
Handlers are handed the provider-stripped model, and for providers that route through
|
||||
a sub-path (``bedrock/mantle/...``, ``openrouter/openai/...``) that remainder still
|
||||
holds a slash. Treating any slash as "already qualified" drops the prefix and leaves a
|
||||
string no provider can be resolved from.
|
||||
"""
|
||||
if not custom_llm_provider or model.startswith(f"{custom_llm_provider}/"):
|
||||
return model
|
||||
return f"{custom_llm_provider}/{model}"
|
||||
|
||||
|
||||
def safe_divide_seconds(seconds: float, denominator: float, default: float | None = None) -> float | None:
|
||||
"""
|
||||
Safely divide seconds by denominator, handling zero division.
|
||||
|
|
|
|||
|
|
@ -5599,9 +5599,9 @@ class BaseLLMHTTPHandler:
|
|||
if patch.messages is None:
|
||||
raise ValueError("Agentic loop plan missing patched messages")
|
||||
|
||||
full_model_name = patch.model or model
|
||||
if "/" not in full_model_name:
|
||||
full_model_name = f"{custom_llm_provider}/{full_model_name}"
|
||||
from litellm.litellm_core_utils.core_helpers import qualify_provider_stripped_model
|
||||
|
||||
full_model_name: Final = qualify_provider_stripped_model(patch.model or model, custom_llm_provider)
|
||||
|
||||
optional_params_for_followup: Final = dict(optional_params)
|
||||
optional_params_for_followup.update(patch.optional_params)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from litellm.litellm_core_utils.core_helpers import (
|
|||
get_or_create_metadata_bucket,
|
||||
map_finish_reason,
|
||||
normalize_drop_params,
|
||||
qualify_provider_stripped_model,
|
||||
reconstruct_model_name,
|
||||
redact_nested_match_and_regex_keys,
|
||||
)
|
||||
|
|
@ -406,3 +407,43 @@ class TestIsExpectedClientError:
|
|||
category=RateLimitErrorCategory.VENDOR_RATE_LIMIT,
|
||||
)
|
||||
assert is_expected_client_error(vendor_limit) is False
|
||||
|
||||
|
||||
class TestQualifyProviderStrippedModel:
|
||||
"""#38829: an agentic follow-up re-dispatched the provider-stripped model. For providers
|
||||
that route through a sub-path the remainder still holds a slash, and the old
|
||||
"any slash means already qualified" test dropped the prefix, leaving a string
|
||||
litellm.acompletion could not resolve a provider from."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model,provider,expected",
|
||||
[
|
||||
# the reported case: bedrock's OpenAI-compatible sub-path
|
||||
("mantle/anthropic.claude-sonnet-5", "bedrock", "bedrock/mantle/anthropic.claude-sonnet-5"),
|
||||
("invoke/anthropic.claude-v2", "bedrock", "bedrock/invoke/anthropic.claude-v2"),
|
||||
# another provider whose stripped model keeps a slash
|
||||
("openai/gpt-4o", "openrouter", "openrouter/openai/gpt-4o"),
|
||||
# the ordinary case still works
|
||||
("gpt-4o", "openai", "openai/gpt-4o"),
|
||||
("claude-sonnet-4-5", "anthropic", "anthropic/claude-sonnet-4-5"),
|
||||
],
|
||||
)
|
||||
def test_the_provider_prefix_is_restored(self, model, provider, expected):
|
||||
assert qualify_provider_stripped_model(model, provider) == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model,provider",
|
||||
[
|
||||
("bedrock/mantle/anthropic.claude-sonnet-5", "bedrock"),
|
||||
("openai/gpt-4o", "openai"),
|
||||
],
|
||||
)
|
||||
def test_an_already_qualified_model_is_left_alone(self, model, provider):
|
||||
assert qualify_provider_stripped_model(model, provider) == model
|
||||
|
||||
def test_a_provider_that_only_shares_a_prefix_is_still_qualified(self):
|
||||
"""`openai` must not be read as a prefix of `openai_like`."""
|
||||
assert qualify_provider_stripped_model("openai_like/foo", "openai") == "openai/openai_like/foo"
|
||||
|
||||
def test_no_provider_leaves_the_model_untouched(self):
|
||||
assert qualify_provider_stripped_model("gpt-4o", "") == "gpt-4o"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue