mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-25 05:54:53 +00:00
Merge 104838d599 into 8b9cd12a89
This commit is contained in:
commit
d445aeae07
8 changed files with 321 additions and 152 deletions
1
.github/workflows/test-unit.yml
vendored
1
.github/workflows/test-unit.yml
vendored
|
|
@ -414,6 +414,7 @@ jobs:
|
|||
tests/unit/images
|
||||
tests/unit/interactions
|
||||
tests/unit/messages
|
||||
tests/unit/native
|
||||
tests/unit/ocr
|
||||
tests/unit/passthrough
|
||||
tests/unit/rag
|
||||
|
|
|
|||
|
|
@ -28,14 +28,15 @@ from litellm.llms.base_llm.anthropic_messages.transformation import (
|
|||
from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
|
||||
from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler
|
||||
from litellm.native.route_plan import NativeChat, NativeResponses, native_messages_config, plan_messages_route
|
||||
from litellm.types.llms.anthropic import AllAnthropicToolsValues
|
||||
from litellm.types.llms.anthropic_messages.anthropic_request import AnthropicMetadata
|
||||
from litellm.types.llms.anthropic_messages.anthropic_response import (
|
||||
AnthropicMessagesResponse,
|
||||
)
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
from litellm.types.utils import CallTypes, LlmProviders
|
||||
from litellm.utils import ProviderConfigManager, client
|
||||
from litellm.types.utils import CallTypes
|
||||
from litellm.utils import client
|
||||
|
||||
from ..adapters.handler import LiteLLMMessagesToCompletionTransformationHandler, request_safeguards_evaluator
|
||||
from ..responses_adapters.handler import LiteLLMMessagesToResponsesAPIHandler
|
||||
|
|
@ -50,62 +51,10 @@ from .utils import AnthropicMessagesRequestUtils, mock_response, prepare_native_
|
|||
|
||||
__all__ = ("anthropic_messages", "anthropic_messages_handler")
|
||||
|
||||
# Providers that are routed directly to the OpenAI Responses API instead of
|
||||
# going through chat/completions.
|
||||
_RESPONSES_API_PROVIDERS: Final = frozenset({"openai"})
|
||||
_SAFEGUARD_KWARGS: Final = TypeAdapter(Mapping[str, object])
|
||||
_SAFEGUARD_MESSAGES: Final = TypeAdapter(Sequence[Mapping[str, object]])
|
||||
|
||||
|
||||
def _bridges_to_responses_api(model: str, custom_llm_provider: str, deployment_model_info: object) -> bool:
|
||||
from litellm.responses.bridge_check import responses_api_bridge_check, str_keyed_mapping
|
||||
|
||||
model_info, _ = responses_api_bridge_check(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
deployment_model_info=str_keyed_mapping(deployment_model_info),
|
||||
)
|
||||
return model_info.get("mode") == "responses"
|
||||
|
||||
|
||||
def _responses_mode_is_lost_by_prefix_strip(
|
||||
requested_model: str, resolved_model: str, custom_llm_provider: str, deployment_model_info: object
|
||||
) -> bool:
|
||||
"""Whether a Responses-only deployment stops looking like one once its provider prefix is stripped.
|
||||
|
||||
``litellm.completion`` re-derives the Responses bridge from the stripped id alone, so a
|
||||
deployment id such as ``perplexity/perplexity/sonar`` (mode ``responses``) is shadowed by the
|
||||
chat entry ``perplexity/sonar`` and would otherwise be sent to chat/completions.
|
||||
"""
|
||||
if requested_model == resolved_model:
|
||||
return False
|
||||
return _bridges_to_responses_api(
|
||||
requested_model, custom_llm_provider, deployment_model_info
|
||||
) and not _bridges_to_responses_api(resolved_model, custom_llm_provider, deployment_model_info)
|
||||
|
||||
|
||||
def _should_route_to_responses_api(
|
||||
custom_llm_provider: str | None,
|
||||
requested_model: str | None = None,
|
||||
resolved_model: str | None = None,
|
||||
deployment_model_info: object = None,
|
||||
) -> bool:
|
||||
"""Return True when the request should use the Responses API path.
|
||||
|
||||
Set ``litellm.use_chat_completions_url_for_anthropic_messages = True`` to
|
||||
opt out and route OpenAI/Azure requests through chat/completions instead.
|
||||
"""
|
||||
if litellm.use_chat_completions_url_for_anthropic_messages:
|
||||
return False
|
||||
if custom_llm_provider in _RESPONSES_API_PROVIDERS:
|
||||
return True
|
||||
if custom_llm_provider is None or requested_model is None or resolved_model is None:
|
||||
return False
|
||||
return _responses_mode_is_lost_by_prefix_strip(
|
||||
requested_model, resolved_model, custom_llm_provider, deployment_model_info
|
||||
)
|
||||
|
||||
|
||||
async def _with_native_safeguard_results(
|
||||
pending: Coroutine[object, object, AnthropicMessagesResponse | AsyncIterator[bytes]],
|
||||
proxy_classifies: Callable[[], Awaitable[bool]],
|
||||
|
|
@ -133,46 +82,6 @@ def _native_safeguards_evaluator(kwargs: Mapping[str, object], messages: object)
|
|||
return request_safeguards_evaluator(kwargs, _SAFEGUARD_MESSAGES.validate_python(messages))
|
||||
|
||||
|
||||
def _deployment_passes_through_anthropic_messages(model_info: object) -> bool:
|
||||
"""Whether the deployment opted into forwarding /v1/messages untranslated.
|
||||
|
||||
The opt-in is ``model_info.supported_endpoints`` containing ``"/v1/messages"``,
|
||||
declared per deployment in config.yaml and plumbed here as ``kwargs["model_info"]``
|
||||
by the router.
|
||||
"""
|
||||
if not isinstance(model_info, dict):
|
||||
return False
|
||||
supported_endpoints: Final = model_info.get("supported_endpoints")
|
||||
return isinstance(supported_endpoints, (list, tuple)) and "/v1/messages" in supported_endpoints
|
||||
|
||||
|
||||
def _deployment_supports_cache_control_ttl(model_info: object) -> bool:
|
||||
return isinstance(model_info, dict) and model_info.get("cache_control_ttl") is True
|
||||
|
||||
|
||||
def _native_messages_config(
|
||||
model: str, custom_llm_provider: str | None, model_info: object, api_base: str | None
|
||||
) -> BaseAnthropicMessagesConfig | None:
|
||||
provider_config: Final = (
|
||||
ProviderConfigManager.get_provider_anthropic_messages_config(
|
||||
model=model,
|
||||
provider=litellm.LlmProviders(custom_llm_provider),
|
||||
)
|
||||
if custom_llm_provider is not None and custom_llm_provider in [provider.value for provider in LlmProviders]
|
||||
else None
|
||||
)
|
||||
native_config: Final = (
|
||||
provider_config if provider_config is not None and provider_config.serves_api_base(model, api_base) else None
|
||||
)
|
||||
if native_config is not None or not _deployment_passes_through_anthropic_messages(model_info):
|
||||
return native_config
|
||||
from litellm.llms.openai_like.messages.transformation import (
|
||||
OpenAILikeAnthropicMessagesConfig,
|
||||
)
|
||||
|
||||
return OpenAILikeAnthropicMessagesConfig(cache_control_ttl=_deployment_supports_cache_control_ttl(model_info))
|
||||
|
||||
|
||||
async def _proxy_classifies(model: str, safeguards: object, native_config: BaseAnthropicMessagesConfig | None) -> bool:
|
||||
if not proxy_serves_safeguards(safeguards):
|
||||
return False
|
||||
|
|
@ -189,7 +98,7 @@ async def proxy_classifies_tool_uses(model: str, custom_llm_provider: str | None
|
|||
return await _proxy_classifies(
|
||||
model,
|
||||
kwargs.get("safeguards"),
|
||||
_native_messages_config(
|
||||
native_messages_config(
|
||||
model, custom_llm_provider, kwargs.get("model_info"), api_base if isinstance(api_base, str) else None
|
||||
),
|
||||
)
|
||||
|
|
@ -636,40 +545,43 @@ def anthropic_messages_handler(
|
|||
**kwargs,
|
||||
)
|
||||
|
||||
anthropic_messages_provider_config: Final = _native_messages_config(
|
||||
model, custom_llm_provider, kwargs.get("model_info"), api_base
|
||||
route: Final = plan_messages_route(
|
||||
requested_model=original_model,
|
||||
resolved_model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
model_info=kwargs.get("model_info"),
|
||||
api_base=api_base,
|
||||
compaction_requested=kwargs.get("compaction") is not None,
|
||||
force_chat_completions=bool(litellm.use_chat_completions_url_for_anthropic_messages),
|
||||
)
|
||||
if anthropic_messages_provider_config is None:
|
||||
# Route to Responses API for OpenAI / Azure, chat/completions for everything else.
|
||||
if kwargs.get("compaction") is None and _should_route_to_responses_api(
|
||||
custom_llm_provider, original_model, model, kwargs.get("model_info")
|
||||
):
|
||||
return LiteLLMMessagesToResponsesAPIHandler.anthropic_messages_handler(
|
||||
max_tokens=max_tokens,
|
||||
messages=messages,
|
||||
model=original_model,
|
||||
metadata=metadata,
|
||||
stop_sequences=stop_sequences,
|
||||
stream=stream,
|
||||
system=cast( # cast-ok: anthropic accepts list system blocks; handler signature is narrower
|
||||
"str | None", system
|
||||
),
|
||||
temperature=temperature,
|
||||
thinking=thinking,
|
||||
tool_choice=tool_choice,
|
||||
tools=cast( # cast-ok: dict tools already fit the handler's union
|
||||
"list[AllAnthropicToolsValues | dict[str, object]] | None", tools
|
||||
),
|
||||
top_k=top_k,
|
||||
top_p=top_p,
|
||||
_is_async=is_async,
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
client=client,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
**kwargs,
|
||||
)
|
||||
if isinstance(route, NativeResponses):
|
||||
return LiteLLMMessagesToResponsesAPIHandler.anthropic_messages_handler(
|
||||
max_tokens=max_tokens,
|
||||
messages=messages,
|
||||
model=original_model,
|
||||
metadata=metadata,
|
||||
stop_sequences=stop_sequences,
|
||||
stream=stream,
|
||||
system=cast( # cast-ok: anthropic accepts list system blocks; handler signature is narrower
|
||||
"str | None", system
|
||||
),
|
||||
temperature=temperature,
|
||||
thinking=thinking,
|
||||
tool_choice=tool_choice,
|
||||
tools=cast( # cast-ok: dict tools already fit the handler's union
|
||||
"list[AllAnthropicToolsValues | dict[str, object]] | None", tools
|
||||
),
|
||||
top_k=top_k,
|
||||
top_p=top_p,
|
||||
_is_async=is_async,
|
||||
api_key=api_key,
|
||||
api_base=api_base,
|
||||
client=client,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if isinstance(route, NativeChat):
|
||||
# The in-gateway context_management polyfill runs inside
|
||||
# ``async_anthropic_messages_handler`` so it can ``await`` the
|
||||
# summarization model for ``compact_20260112``. ``context_management``
|
||||
|
|
@ -698,6 +610,7 @@ def anthropic_messages_handler(
|
|||
**kwargs,
|
||||
)
|
||||
|
||||
anthropic_messages_provider_config: Final = route.config
|
||||
if custom_llm_provider is None:
|
||||
raise ValueError(
|
||||
f"custom_llm_provider is required for Anthropic messages, passed in model={model}, custom_llm_provider={custom_llm_provider}"
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ from litellm.llms.vertex_ai.common_utils import (
|
|||
VertexAIModelRoute,
|
||||
get_vertex_ai_model_route,
|
||||
)
|
||||
from litellm.native.route_plan import NativeResponses, plan_chat_route
|
||||
from litellm.realtime_api.main import realtime_health_check
|
||||
from litellm.secret_managers.main import get_secret_bool, get_secret_str
|
||||
from litellm.types.completion import (
|
||||
|
|
@ -6026,7 +6027,10 @@ def completion(
|
|||
# detection when the deployment name differs from the model name.
|
||||
_azure_detection_model: Final = base_model or model
|
||||
|
||||
if responses_api_model_info.get("mode") == "responses" and not skip_responses_api_bridge:
|
||||
if isinstance(
|
||||
plan_chat_route(responses_api_model_info.get("mode") == "responses", skip_responses_api_bridge is True),
|
||||
NativeResponses,
|
||||
):
|
||||
from litellm.completion_extras import responses_api_bridge
|
||||
|
||||
optional_params, rs_val = strip_reasoning_summary_aliases_from_optional_params(optional_params)
|
||||
|
|
|
|||
162
litellm/native/route_plan.py
Normal file
162
litellm/native/route_plan.py
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
"""Which native API format a request reaches its provider in, decided once per request by the client-facing surface."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Final, TypeAlias
|
||||
|
||||
from litellm.llms.base_llm.anthropic_messages.transformation import BaseAnthropicMessagesConfig
|
||||
from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig
|
||||
from litellm.types.utils import LlmProviders
|
||||
from litellm.utils import ProviderConfigManager
|
||||
|
||||
__all__ = (
|
||||
"NativeChat",
|
||||
"NativeMessages",
|
||||
"NativeResponses",
|
||||
"RoutePlan",
|
||||
"native_messages_config",
|
||||
"plan_chat_route",
|
||||
"plan_messages_route",
|
||||
"plan_responses_route",
|
||||
)
|
||||
|
||||
_MESSAGES_TO_RESPONSES_PROVIDERS: Final = frozenset({"openai"})
|
||||
_PROVIDER_VALUES: Final = frozenset(provider.value for provider in LlmProviders)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class NativeMessages:
|
||||
config: BaseAnthropicMessagesConfig
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class NativeResponses:
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class NativeChat:
|
||||
pass
|
||||
|
||||
|
||||
RoutePlan: TypeAlias = NativeMessages | NativeResponses | NativeChat
|
||||
|
||||
|
||||
def _deployment_passes_through_anthropic_messages(model_info: object) -> bool:
|
||||
"""Whether the deployment opted into forwarding /v1/messages untranslated.
|
||||
|
||||
The opt-in is ``model_info.supported_endpoints`` containing ``"/v1/messages"``,
|
||||
declared per deployment in config.yaml and plumbed here as ``kwargs["model_info"]``
|
||||
by the router.
|
||||
"""
|
||||
if not isinstance(model_info, dict):
|
||||
return False
|
||||
supported_endpoints: Final = model_info.get("supported_endpoints")
|
||||
return isinstance(supported_endpoints, (list, tuple)) and "/v1/messages" in supported_endpoints
|
||||
|
||||
|
||||
def _deployment_supports_cache_control_ttl(model_info: object) -> bool:
|
||||
return isinstance(model_info, dict) and model_info.get("cache_control_ttl") is True
|
||||
|
||||
|
||||
def native_messages_config(
|
||||
model: str, custom_llm_provider: str | None, model_info: object, api_base: str | None
|
||||
) -> BaseAnthropicMessagesConfig | None:
|
||||
provider_config: Final = (
|
||||
ProviderConfigManager.get_provider_anthropic_messages_config(
|
||||
model=model,
|
||||
provider=LlmProviders(custom_llm_provider),
|
||||
)
|
||||
if custom_llm_provider is not None and custom_llm_provider in _PROVIDER_VALUES
|
||||
else None
|
||||
)
|
||||
native_config: Final = (
|
||||
provider_config if provider_config is not None and provider_config.serves_api_base(model, api_base) else None
|
||||
)
|
||||
if native_config is not None or not _deployment_passes_through_anthropic_messages(model_info):
|
||||
return native_config
|
||||
from litellm.llms.openai_like.messages.transformation import (
|
||||
OpenAILikeAnthropicMessagesConfig,
|
||||
)
|
||||
|
||||
return OpenAILikeAnthropicMessagesConfig(cache_control_ttl=_deployment_supports_cache_control_ttl(model_info))
|
||||
|
||||
|
||||
def _bridges_to_responses_api(model: str, custom_llm_provider: str, deployment_model_info: object) -> bool:
|
||||
from litellm.responses.bridge_check import responses_api_bridge_check, str_keyed_mapping
|
||||
|
||||
model_info, _ = responses_api_bridge_check(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
deployment_model_info=str_keyed_mapping(deployment_model_info),
|
||||
)
|
||||
return model_info.get("mode") == "responses"
|
||||
|
||||
|
||||
def _responses_mode_is_lost_by_prefix_strip(
|
||||
requested_model: str, resolved_model: str, custom_llm_provider: str, deployment_model_info: object
|
||||
) -> bool:
|
||||
"""Whether a Responses-only deployment stops looking like one once its provider prefix is stripped.
|
||||
|
||||
``litellm.completion`` re-derives the Responses bridge from the stripped id alone, so a
|
||||
deployment id such as ``perplexity/perplexity/sonar`` (mode ``responses``) is shadowed by the
|
||||
chat entry ``perplexity/sonar`` and would otherwise be sent to chat/completions.
|
||||
"""
|
||||
if requested_model == resolved_model:
|
||||
return False
|
||||
return _bridges_to_responses_api(
|
||||
requested_model, custom_llm_provider, deployment_model_info
|
||||
) and not _bridges_to_responses_api(resolved_model, custom_llm_provider, deployment_model_info)
|
||||
|
||||
|
||||
def _messages_bridge_to_responses(
|
||||
custom_llm_provider: str | None,
|
||||
requested_model: str,
|
||||
resolved_model: str,
|
||||
deployment_model_info: object,
|
||||
force_chat_completions: bool,
|
||||
) -> bool:
|
||||
if force_chat_completions:
|
||||
return False
|
||||
if custom_llm_provider in _MESSAGES_TO_RESPONSES_PROVIDERS:
|
||||
return True
|
||||
if custom_llm_provider is None:
|
||||
return False
|
||||
return _responses_mode_is_lost_by_prefix_strip(
|
||||
requested_model, resolved_model, custom_llm_provider, deployment_model_info
|
||||
)
|
||||
|
||||
|
||||
def plan_messages_route(
|
||||
*,
|
||||
requested_model: str,
|
||||
resolved_model: str,
|
||||
custom_llm_provider: str | None,
|
||||
model_info: object,
|
||||
api_base: str | None,
|
||||
compaction_requested: bool,
|
||||
force_chat_completions: bool,
|
||||
) -> RoutePlan:
|
||||
"""``force_chat_completions`` is ``litellm.use_chat_completions_url_for_anthropic_messages``."""
|
||||
config: Final = native_messages_config(resolved_model, custom_llm_provider, model_info, api_base)
|
||||
if config is not None:
|
||||
return NativeMessages(config)
|
||||
if not compaction_requested and _messages_bridge_to_responses(
|
||||
custom_llm_provider, requested_model, resolved_model, model_info, force_chat_completions
|
||||
):
|
||||
return NativeResponses()
|
||||
return NativeChat()
|
||||
|
||||
|
||||
def plan_responses_route(
|
||||
responses_api_provider_config: BaseResponsesAPIConfig | None, use_chat_completions_api: bool
|
||||
) -> NativeResponses | NativeChat:
|
||||
if responses_api_provider_config is None or use_chat_completions_api:
|
||||
return NativeChat()
|
||||
return NativeResponses()
|
||||
|
||||
|
||||
def plan_chat_route(responses_mode_model: bool, skip_responses_api_bridge: bool) -> NativeChat | NativeResponses:
|
||||
"""``responses_mode_model`` is whether ``responses_api_bridge_check`` resolved the deployment to mode ``responses``."""
|
||||
if responses_mode_model and not skip_responses_api_bridge:
|
||||
return NativeResponses()
|
||||
return NativeChat()
|
||||
|
|
@ -40,6 +40,7 @@ from litellm.litellm_core_utils.provider_affinity import add_provider_affinity_h
|
|||
from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig
|
||||
from litellm.llms.custom_httpx.llm_http_handler import BaseLLMHTTPHandler
|
||||
from litellm.llms.openai_like.responses.transformation import OpenAILikeResponsesConfig
|
||||
from litellm.native.route_plan import NativeChat, plan_responses_route
|
||||
from litellm.responses.litellm_completion_transformation.handler import (
|
||||
LiteLLMCompletionTransformationHandler,
|
||||
)
|
||||
|
|
@ -532,13 +533,6 @@ async def aresponses_api_with_mcp(
|
|||
return response
|
||||
|
||||
|
||||
def _bridges_to_chat_completions(
|
||||
responses_api_provider_config: BaseResponsesAPIConfig | None, use_chat_completions_api: bool
|
||||
) -> bool:
|
||||
"""Whether the request reaches its provider as a chat completion, not a Responses call."""
|
||||
return responses_api_provider_config is None or use_chat_completions_api is True
|
||||
|
||||
|
||||
_RESPONSES_ONLY_REQUEST_FIELDS_NEVER_BRIDGED: Final = frozenset({"client_metadata"})
|
||||
|
||||
|
||||
|
|
@ -583,7 +577,7 @@ def _encrypted_task_support_failure(
|
|||
) -> _ResponsesCompatibilityFailure | None:
|
||||
if (
|
||||
responses_api_provider_config is None
|
||||
or _bridges_to_chat_completions(responses_api_provider_config, use_chat_completions_api)
|
||||
or isinstance(plan_responses_route(responses_api_provider_config, use_chat_completions_api), NativeChat)
|
||||
or not responses_api_provider_config.supports_encrypted_agent_messages()
|
||||
):
|
||||
return "encrypted_task_unsupported"
|
||||
|
|
@ -657,7 +651,7 @@ def _will_bridge_to_chat_completions(
|
|||
model_info: object,
|
||||
api_base: str | None,
|
||||
) -> bool:
|
||||
"""``_bridges_to_chat_completions`` for callers running before the provider config is resolved.
|
||||
"""``plan_responses_route`` for callers running before the provider config is resolved.
|
||||
|
||||
Resolving the config is a pure lookup, so this asks the same question the dispatch
|
||||
asks rather than restating its condition, with the model name the dispatch hands the
|
||||
|
|
@ -670,14 +664,17 @@ def _will_bridge_to_chat_completions(
|
|||
normalized_model: Final = _normalize_openai_chat_completions_responses_model(model)
|
||||
if custom_llm_provider is None:
|
||||
return True
|
||||
return _bridges_to_chat_completions(
|
||||
_resolve_responses_api_provider_config(
|
||||
_dispatched_model_name(normalized_model[0], custom_llm_provider, api_base),
|
||||
custom_llm_provider,
|
||||
model_info,
|
||||
api_base,
|
||||
return isinstance(
|
||||
plan_responses_route(
|
||||
_resolve_responses_api_provider_config(
|
||||
_dispatched_model_name(normalized_model[0], custom_llm_provider, api_base),
|
||||
custom_llm_provider,
|
||||
model_info,
|
||||
api_base,
|
||||
),
|
||||
use_chat_completions_api or normalized_model[1],
|
||||
),
|
||||
use_chat_completions_api or normalized_model[1],
|
||||
NativeChat,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1604,7 +1601,7 @@ def responses(
|
|||
if _file_search_dispatch is not None:
|
||||
return _file_search_dispatch
|
||||
|
||||
if _bridges_to_chat_completions(responses_api_provider_config, use_chat_completions_api):
|
||||
if isinstance(plan_responses_route(responses_api_provider_config, use_chat_completions_api), NativeChat):
|
||||
bridge_kwargs: Final = _bridge_kwargs(kwargs, responses_api_provider_config, allowed_openai_params)
|
||||
return litellm_completion_transformation_handler.response_api_handler(
|
||||
_previous_response_client_ids(previous_response_id, kwargs),
|
||||
|
|
|
|||
|
|
@ -32,13 +32,7 @@ def _call_handler_and_capture_optional_params(thinking=None, **extra_kwargs):
|
|||
with patch(
|
||||
"litellm.llms.anthropic.pass_through.messages.handler."
|
||||
"base_llm_http_handler"
|
||||
) as mock_handler, patch(
|
||||
"litellm.llms.anthropic.pass_through.messages.handler."
|
||||
"ProviderConfigManager"
|
||||
) as mock_pcm:
|
||||
# Make get_provider_anthropic_messages_config return a non-None config
|
||||
# so the handler takes the native Anthropic path
|
||||
mock_pcm.get_provider_anthropic_messages_config.return_value = MagicMock()
|
||||
) as mock_handler:
|
||||
mock_handler.anthropic_messages_handler.return_value = MagicMock()
|
||||
|
||||
kwargs = dict(extra_kwargs)
|
||||
|
|
|
|||
0
tests/unit/native/__init__.py
Normal file
0
tests/unit/native/__init__.py
Normal file
98
tests/unit/native/test_route_plan.py
Normal file
98
tests/unit/native/test_route_plan.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import pytest
|
||||
|
||||
from litellm.llms.anthropic.pass_through.messages.transformation import AnthropicMessagesConfig
|
||||
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
|
||||
from litellm.llms.openai_like.messages.transformation import OpenAILikeAnthropicMessagesConfig
|
||||
from litellm.native.route_plan import (
|
||||
NativeChat,
|
||||
NativeMessages,
|
||||
NativeResponses,
|
||||
RoutePlan,
|
||||
plan_chat_route,
|
||||
plan_messages_route,
|
||||
plan_responses_route,
|
||||
)
|
||||
|
||||
MESSAGES_PASSTHROUGH: dict[str, object] = {"supported_endpoints": ["/v1/messages"]}
|
||||
|
||||
|
||||
def _plan(
|
||||
model: str,
|
||||
provider: str,
|
||||
*,
|
||||
model_info: object = None,
|
||||
compaction_requested: bool = False,
|
||||
force_chat_completions: bool = False,
|
||||
) -> RoutePlan:
|
||||
return plan_messages_route(
|
||||
requested_model=f"{provider}/{model}",
|
||||
resolved_model=model,
|
||||
custom_llm_provider=provider,
|
||||
model_info=model_info,
|
||||
api_base=None,
|
||||
compaction_requested=compaction_requested,
|
||||
force_chat_completions=force_chat_completions,
|
||||
)
|
||||
|
||||
|
||||
def test_a_provider_that_speaks_messages_gets_its_own_messages_config():
|
||||
plan = _plan("claude-sonnet-4-5", "anthropic")
|
||||
|
||||
assert isinstance(plan, NativeMessages)
|
||||
assert isinstance(plan.config, AnthropicMessagesConfig)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model", "provider", "kwargs", "expected"),
|
||||
[
|
||||
("gpt-5.5", "openai", {}, NativeResponses()),
|
||||
("gpt-5.5", "openai", {"force_chat_completions": True}, NativeChat()),
|
||||
("gpt-5.5", "openai", {"compaction_requested": True}, NativeChat()),
|
||||
("my-model", "hosted_vllm", {}, NativeChat()),
|
||||
],
|
||||
)
|
||||
def test_messages_without_a_native_config_bridge_to_responses_only_for_openai(
|
||||
model: str, provider: str, kwargs: dict[str, bool], expected: RoutePlan
|
||||
):
|
||||
assert _plan(model, provider, **kwargs) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cache_control_ttl", [True, False])
|
||||
def test_a_deployment_that_opts_into_messages_passthrough_skips_translation(cache_control_ttl: bool):
|
||||
plan = _plan("my-model", "hosted_vllm", model_info={**MESSAGES_PASSTHROUGH, "cache_control_ttl": cache_control_ttl})
|
||||
|
||||
assert isinstance(plan, NativeMessages)
|
||||
assert isinstance(plan.config, OpenAILikeAnthropicMessagesConfig)
|
||||
assert plan.config.supports_cache_control_ttl() is cache_control_ttl
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("has_config", "use_chat_completions_api", "expected"),
|
||||
[
|
||||
(True, False, NativeResponses()),
|
||||
(True, True, NativeChat()),
|
||||
(False, False, NativeChat()),
|
||||
(False, True, NativeChat()),
|
||||
],
|
||||
)
|
||||
def test_responses_reach_chat_without_a_provider_config_or_when_asked(
|
||||
has_config: bool, use_chat_completions_api: bool, expected: RoutePlan
|
||||
):
|
||||
config = OpenAIResponsesAPIConfig() if has_config else None
|
||||
|
||||
assert plan_responses_route(config, use_chat_completions_api) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("responses_mode_model", "skip_responses_api_bridge", "expected"),
|
||||
[
|
||||
(True, False, NativeResponses()),
|
||||
(True, True, NativeChat()),
|
||||
(False, False, NativeChat()),
|
||||
(False, True, NativeChat()),
|
||||
],
|
||||
)
|
||||
def test_chat_bridges_to_responses_only_for_responses_mode_models(
|
||||
responses_mode_model: bool, skip_responses_api_bridge: bool, expected: RoutePlan
|
||||
):
|
||||
assert plan_chat_route(responses_mode_model, skip_responses_api_bridge) == expected
|
||||
Loading…
Add table
Reference in a new issue