This commit is contained in:
Priyansh Nandwana 2026-09-12 14:51:52 -04:00 committed by GitHub
commit ddf4cecd3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 91 additions and 8 deletions

View file

@ -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",

View file

@ -29,6 +29,13 @@ 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 provider-stripped model."""
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.

View file

@ -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)

View file

@ -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,36 @@ class TestIsExpectedClientError:
category=RateLimitErrorCategory.VENDOR_RATE_LIMIT,
)
assert is_expected_client_error(vendor_limit) is False
class TestQualifyProviderStrippedModel:
"""#38829: a model whose remainder still holds a slash must keep its provider prefix."""
@pytest.mark.parametrize(
"model,provider,expected",
[
("mantle/anthropic.claude-sonnet-5", "bedrock", "bedrock/mantle/anthropic.claude-sonnet-5"),
("invoke/anthropic.claude-v2", "bedrock", "bedrock/invoke/anthropic.claude-v2"),
("openai/gpt-4o", "openrouter", "openrouter/openai/gpt-4o"),
("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):
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"

View file

@ -3713,3 +3713,47 @@ def test_image_edit_handler_keeps_the_sync_transform():
assert config.transform_calls == ["sync"]
assert captured["body"] == {"transformed_by": "sync"}
assert response.data[0].b64_json == "sync"
class TestAgenticFollowUpKeepsTheProviderPrefix:
"""#38829: the follow-up must re-dispatch a model litellm.acompletion can route."""
@staticmethod
def _plan():
from litellm.types.integrations.custom_logger import (
AgenticLoopPlan,
AgenticLoopRequestPatch,
)
return AgenticLoopPlan(
run_agentic_loop=True,
request_patch=AgenticLoopRequestPatch(messages=[{"role": "user", "content": "hi"}]),
)
async def _run_followup(self, model: str, custom_llm_provider: str):
from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler
return await BaseLLMHTTPHandler()._execute_chat_completion_agentic_plan(
plan=self._plan(),
model=model,
messages=[{"role": "user", "content": "hi"}],
optional_params={"mock_response": "ok from followup"},
kwargs={},
custom_llm_provider=custom_llm_provider,
depth=0,
max_loops=2,
fingerprints=[],
fingerprint="fp",
)
@pytest.mark.asyncio
async def test_a_sub_path_model_still_resolves_a_provider(self):
response = await self._run_followup("mantle/anthropic.claude-sonnet-5", "bedrock")
assert response.choices[0].message.content == "ok from followup"
@pytest.mark.asyncio
async def test_an_ordinary_model_still_resolves_a_provider(self):
response = await self._run_followup("gpt-4o-mini", "openai")
assert response.choices[0].message.content == "ok from followup"