mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
Merge cc56e3c43b into abc85c2651
This commit is contained in:
commit
e7cf284c53
6 changed files with 142 additions and 11 deletions
|
|
@ -1925,11 +1925,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",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from litellm.litellm_core_utils.agentic_loop_settings import (
|
|||
DEFAULT_MAX_AGENTIC_LOOPS,
|
||||
validated_max_agentic_loops,
|
||||
)
|
||||
from litellm.litellm_core_utils.core_helpers import qualify_agentic_followup_model
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObject
|
||||
from litellm.llms.base_llm.base_model_iterator import MockResponseIterator
|
||||
from litellm.types.integrations.custom_logger import (
|
||||
|
|
@ -170,9 +171,7 @@ async def _execute_chat_completion_agentic_plan(
|
|||
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}"
|
||||
full_model_name: Final = qualify_agentic_followup_model(patch.model, model, custom_llm_provider)
|
||||
|
||||
optional_params_for_followup: Final = {**optional_params, **patch.optional_params}
|
||||
if patch.tools is not None:
|
||||
|
|
|
|||
|
|
@ -30,6 +30,26 @@ 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 qualify_agentic_followup_model(patch_model: str | None, model: str, custom_llm_provider: str) -> str:
|
||||
"""Resolve the model an agentic follow-up re-dispatches.
|
||||
|
||||
A hook that qualified its own patched model owns that choice, including a cross-provider
|
||||
one, so only a bare patched model and the request's provider-stripped model get a prefix.
|
||||
"""
|
||||
if patch_model is None:
|
||||
return qualify_provider_stripped_model(model, custom_llm_provider)
|
||||
if "/" in patch_model:
|
||||
return patch_model
|
||||
return qualify_provider_stripped_model(patch_model, custom_llm_provider)
|
||||
|
||||
|
||||
def safe_divide_seconds(seconds: float, denominator: float, default: float | None = None) -> float | None:
|
||||
"""
|
||||
Safely divide seconds by denominator, handling zero division.
|
||||
|
|
|
|||
|
|
@ -5497,9 +5497,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_agentic_followup_model
|
||||
|
||||
full_model_name: Final = qualify_agentic_followup_model(patch.model, model, custom_llm_provider)
|
||||
|
||||
optional_params_for_followup: Final = dict(optional_params)
|
||||
optional_params_for_followup.update(patch.optional_params)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ from litellm.litellm_core_utils.core_helpers import (
|
|||
get_provider_response_headers_from_hidden_params,
|
||||
map_finish_reason,
|
||||
normalize_drop_params,
|
||||
qualify_agentic_followup_model,
|
||||
qualify_provider_stripped_model,
|
||||
reconstruct_model_name,
|
||||
redact_nested_match_and_regex_keys,
|
||||
set_provider_response_headers_in_hidden_params,
|
||||
|
|
@ -557,3 +559,60 @@ class TestProviderResponseHeadersInHiddenParams:
|
|||
|
||||
assert get_provider_response_headers_from_hidden_params(sibling) is None
|
||||
assert "additional_headers" not in sibling._hidden_params
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
class TestQualifyAgenticFollowUpModel:
|
||||
"""A hook that qualified its own patched model owns that choice, so a cross-provider
|
||||
follow-up must not be re-prefixed with the original request's provider."""
|
||||
|
||||
def test_a_cross_provider_patched_model_is_dispatched_as_the_hook_asked(self):
|
||||
assert qualify_agentic_followup_model("anthropic/claude-sonnet-4-5", "gpt-4o", "openai") == (
|
||||
"anthropic/claude-sonnet-4-5"
|
||||
)
|
||||
|
||||
def test_a_bare_patched_model_takes_the_request_provider(self):
|
||||
assert qualify_agentic_followup_model("gpt-4o-mini", "gpt-4o", "openai") == "openai/gpt-4o-mini"
|
||||
|
||||
def test_a_sub_path_request_model_keeps_its_provider(self):
|
||||
assert qualify_agentic_followup_model(None, "mantle/anthropic.claude-sonnet-5", "bedrock") == (
|
||||
"bedrock/mantle/anthropic.claude-sonnet-5"
|
||||
)
|
||||
|
||||
def test_an_unpatched_ordinary_model_takes_the_request_provider(self):
|
||||
assert qualify_agentic_followup_model(None, "gpt-4o", "openai") == "openai/gpt-4o"
|
||||
|
||||
def test_a_patched_model_already_holding_the_request_provider_is_left_alone(self):
|
||||
assert qualify_agentic_followup_model("openai/gpt-4o", "gpt-4o", "openai") == "openai/gpt-4o"
|
||||
|
|
|
|||
|
|
@ -4302,3 +4302,58 @@ async def test_async_text_to_speech_handler_records_upstream_response_headers():
|
|||
|
||||
assert response.content == b"audio-bytes"
|
||||
_assert_upstream_headers_recorded(response)
|
||||
|
||||
|
||||
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, patched_model: str | None = None):
|
||||
from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler
|
||||
|
||||
plan = self._plan()
|
||||
if patched_model is not None:
|
||||
plan.request_patch.model = patched_model
|
||||
|
||||
return await BaseLLMHTTPHandler()._execute_chat_completion_agentic_plan(
|
||||
plan=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"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_hook_that_patches_another_providers_model_reaches_that_provider(self):
|
||||
response = await self._run_followup("gpt-4o", "openai", patched_model="anthropic/claude-sonnet-4-5")
|
||||
|
||||
assert response.model == "claude-sonnet-4-5"
|
||||
assert response._hidden_params["custom_llm_provider"] == "anthropic"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue