refactor(typing): clear about 1,100 basedpyright Any errors across 303 files (#49986)

* refactor(typing): clear basedpyright Any errors (wave 1)

* refactor(typing): clear basedpyright Any errors (wave 2)

* refactor(typing): clear basedpyright Any errors (wave 3)

* refactor(typing): clear basedpyright Any errors (wave 4)

* refactor(typing): clear basedpyright Any errors (wave 5)

* refactor(typing): clear basedpyright Any errors (wave 6)

* fix(typing): keep the cached-plan fallback's direct query_first calls so span naming resolves

* fix(logging): keep callbacks running when litellm_params is a params model

should_run_callback and the sync success and failure handlers read litellm_params through any object with a get method again, as they did before this branch, so a logging hook that returns a GenericLiteLLMParams no longer stops sync logging for that request

Adds behavior tests for the changed lines codecov listed as not run

* fix: read the regional uplift for every data residency again and reuse one dict TypeAdapter

The typed lookup only read the eu and us multipliers, so a deployment that set regional_processing_uplift_multiplier_fedramp stopped applying it. /model/update and the model write paths now share one module-level TypeAdapter instead of building one per call.

* fix: bill regional uplift multipliers float() accepts, bytes included

* fix: raise the in operator's own error from one shared json_contains, and name the bad uplift type
This commit is contained in:
Mateo Wang 2026-11-06 13:31:15 -08:00 • committed by GitHub
parent 2d2290067c
commit 3f38730d2c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
303 changed files with 5111 additions and 1411 deletions

View file

@ -4,13 +4,14 @@ Custom A2A Card Resolver for LiteLLM.
Extends the A2A SDK's card resolver to support multiple well-known paths. Extends the A2A SDK's card resolver to support multiple well-known paths.
""" """
from collections.abc import Mapping from collections.abc import Mapping, Sequence
from types import MappingProxyType from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Final, Protocol, runtime_checkable from typing import TYPE_CHECKING, Any, Final, Protocol, runtime_checkable
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
from litellm.a2a_protocol.exceptions import A2AAgentCardDiscoveryError from litellm.a2a_protocol.exceptions import A2AAgentCardDiscoveryError
from litellm.constants import LOCALHOST_URL_PATTERNS from litellm.constants import LOCALHOST_URL_PATTERNS
from litellm.litellm_core_utils.core_helpers import has_attributes
if TYPE_CHECKING: if TYPE_CHECKING:
from a2a.types import AgentCard from a2a.types import AgentCard
@ -130,15 +131,34 @@ def normalize_agent_card_interfaces(agent_card: "AgentCard", protocol_version: s
return normalized return normalized
class _UrlCarrier(Protocol):
url: str
class _InterfacesCarrier(Protocol):
@property
def supported_interfaces(self) -> Sequence[_UrlCarrier]: ...
def _url_of(value: object) -> str | None:
return value.url if has_attributes(value, _UrlCarrier, "url") else None
def _supported_interfaces(agent_card: object) -> Sequence[_UrlCarrier] | None:
if has_attributes(agent_card, _InterfacesCarrier, "supported_interfaces"):
return agent_card.supported_interfaces
return None
def get_agent_card_url(agent_card: "AgentCard") -> str | None: def get_agent_card_url(agent_card: "AgentCard") -> str | None:
"""Return the agent endpoint URL from the resolved SDK card.""" """Return the agent endpoint URL from the resolved SDK card."""
url: Final = getattr(agent_card, "url", None) url: Final = _url_of(agent_card)
if url: if url:
return url return url
interfaces: Final = getattr(agent_card, "supported_interfaces", None) interfaces: Final = _supported_interfaces(agent_card)
if interfaces: if interfaces:
return getattr(interfaces[0], "url", None) return _url_of(interfaces[0])
return None return None
@ -168,16 +188,16 @@ def fix_agent_card_url(agent_card: "AgentCard", base_url: str) -> "AgentCard":
Returns: Returns:
The agent card with the URL fixed if necessary The agent card with the URL fixed if necessary
""" """
card_url: Final = getattr(agent_card, "url", None) card_url: Final = _url_of(agent_card)
if card_url and is_localhost_or_internal_url(card_url): if card_url and is_localhost_or_internal_url(card_url):
# Normalize base_url to ensure it ends with / # Normalize base_url to ensure it ends with /
fixed_url: Final = base_url.rstrip("/") + "/" fixed_url: Final = base_url.rstrip("/") + "/"
agent_card.url = fixed_url agent_card.url = fixed_url
interfaces: Final = getattr(agent_card, "supported_interfaces", None) interfaces: Final = _supported_interfaces(agent_card)
if interfaces: if interfaces:
interface_url: Final = getattr(interfaces[0], "url", None) interface_url: Final = _url_of(interfaces[0])
if interface_url and is_localhost_or_internal_url(interface_url): if interface_url and is_localhost_or_internal_url(interface_url):
interfaces[0].url = base_url.rstrip("/") + "/" interfaces[0].url = base_url.rstrip("/") + "/"
@ -194,6 +214,9 @@ class LiteLLMA2ACardResolver(_A2ACardResolver):
- /agentCard/v1.0 - /agentCard/v1.0
""" """
if TYPE_CHECKING:
base_url: str
async def get_agent_card( async def get_agent_card(
self, self,
relative_card_path: str | None = None, relative_card_path: str | None = None,

View file

@ -4,7 +4,7 @@ A2A Protocol Exception Mapping Utils.
Maps A2A SDK exceptions to LiteLLM A2A exception types. Maps A2A SDK exceptions to LiteLLM A2A exception types.
""" """
from typing import TYPE_CHECKING, Any, Final from typing import TYPE_CHECKING, Final
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
from litellm.a2a_protocol.card_resolver import ( from litellm.a2a_protocol.card_resolver import (
@ -21,6 +21,7 @@ from litellm.constants import CONNECTION_ERROR_PATTERNS
if TYPE_CHECKING: if TYPE_CHECKING:
from a2a.client import Client as A2AClientType from a2a.client import Client as A2AClientType
from a2a.types import AgentCard
_A2A_KIND_DIALECT_HINT: Final = ( _A2A_KIND_DIALECT_HINT: Final = (
@ -165,7 +166,7 @@ def map_a2a_exception(
async def handle_a2a_localhost_retry( async def handle_a2a_localhost_retry(
error: A2ALocalhostURLError, error: A2ALocalhostURLError,
agent_card: Any, agent_card: "AgentCard | None",
a2a_client: "A2AClientType", a2a_client: "A2AClientType",
is_streaming: bool = False, is_streaming: bool = False,
) -> "A2AClientType": ) -> "A2AClientType":

View file

@ -10,7 +10,7 @@ A2A Streaming Events (in order):
4. Status update (kind: "status-update") - Final status "completed" with final=true 4. Status update (kind: "status-update") - Final status "completed" with final=true
""" """
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping from collections.abc import AsyncIterator, Mapping
from types import MappingProxyType from types import MappingProxyType
from typing import Any, Final from typing import Any, Final
@ -126,10 +126,7 @@ class A2ACompletionBridgeHandler:
@staticmethod @staticmethod
async def _acompletion(completion_params: Mapping[str, object]) -> ModelResponse | CustomStreamWrapper: async def _acompletion(completion_params: Mapping[str, object]) -> ModelResponse | CustomStreamWrapper:
acompletion_fn: Final[Callable[..., Coroutine[object, object, ModelResponse | CustomStreamWrapper]]] = vars( return await litellm.acompletion(**completion_params)
litellm
)["acompletion"]
return await acompletion_fn(**completion_params)
@staticmethod @staticmethod
async def handle_non_streaming( async def handle_non_streaming(

View file

@ -14,7 +14,7 @@ import datetime
import uuid import uuid
from collections.abc import AsyncIterator, Coroutine, Mapping from collections.abc import AsyncIterator, Coroutine, Mapping
from types import MappingProxyType, ModuleType from types import MappingProxyType, ModuleType
from typing import TYPE_CHECKING, Final, Optional, cast from typing import TYPE_CHECKING, Final, Optional, Protocol, cast
from typing_extensions import TypedDict, Unpack from typing_extensions import TypedDict, Unpack
@ -24,6 +24,7 @@ from litellm.a2a_protocol.streaming_iterator import A2AStreamingIterator
from litellm.a2a_protocol.utils import A2ARequestUtils from litellm.a2a_protocol.utils import A2ARequestUtils
from litellm.constants import DEFAULT_A2A_AGENT_TIMEOUT from litellm.constants import DEFAULT_A2A_AGENT_TIMEOUT
from litellm.litellm_core_utils.asyncify import asyncify from litellm.litellm_core_utils.asyncify import asyncify
from litellm.litellm_core_utils.core_helpers import require_attributes
from litellm.litellm_core_utils.litellm_logging import Logging from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.llms.custom_httpx.http_handler import ( from litellm.llms.custom_httpx.http_handler import (
get_async_httpx_client, get_async_httpx_client,
@ -36,7 +37,6 @@ if TYPE_CHECKING:
from a2a.client import Client as A2AClientType from a2a.client import Client as A2AClientType
from a2a.client import ClientCallContext as A2ACallContextType from a2a.client import ClientCallContext as A2ACallContextType
from a2a.compat.v0_3.types import ( from a2a.compat.v0_3.types import (
AgentCard,
Message, Message,
SendMessageRequest, SendMessageRequest,
SendMessageResponse, SendMessageResponse,
@ -45,6 +45,7 @@ if TYPE_CHECKING:
SendStreamingMessageSuccessResponse, SendStreamingMessageSuccessResponse,
Task, Task,
) )
from a2a.types import AgentCard
from a2a.types.a2a_pb2 import SendMessageRequest as CoreSendMessageRequest from a2a.types.a2a_pb2 import SendMessageRequest as CoreSendMessageRequest
from a2a.types.a2a_pb2 import StreamResponse as CoreStreamResponse from a2a.types.a2a_pb2 import StreamResponse as CoreStreamResponse
@ -95,6 +96,12 @@ class _SendMessageOptions(TypedDict, total=False, extra_items=object):
pass pass
class _StreamingRequestConverter(Protocol):
def to_core_send_message_request(
self, compat_req: "SendStreamingMessageRequest", /
) -> "CoreSendMessageRequest": ...
def _set_usage_on_logging_obj( def _set_usage_on_logging_obj(
kwargs: Mapping[str, object], kwargs: Mapping[str, object],
prompt_tokens: int, prompt_tokens: int,
@ -375,7 +382,9 @@ async def _stream_messages(
"The 'a2a' package is required for A2A agent invocation. Install it with: pip install a2a-sdk" "The 'a2a' package is required for A2A agent invocation. Install it with: pip install a2a-sdk"
) )
pb_request: Final[CoreSendMessageRequest] = _a2a_conversions.to_core_send_message_request(request) pb_request: Final[CoreSendMessageRequest] = require_attributes(
_a2a_conversions, _StreamingRequestConverter, "to_core_send_message_request"
).to_core_send_message_request(request)
async for event in a2a_client.send_message(pb_request, context=_get_a2a_call_context(a2a_client)): async for event in a2a_client.send_message(pb_request, context=_get_a2a_call_context(a2a_client)):
compat_chunk = _to_compat_stream_response(event, request_id=request.id) compat_chunk = _to_compat_stream_response(event, request_id=request.id)
yield SendStreamingMessageResponse(root=compat_chunk) yield SendStreamingMessageResponse(root=compat_chunk)

View file

@ -13,10 +13,10 @@ import json
import logging import logging
import time import time
import traceback import traceback
from collections.abc import Generator, Mapping from collections.abc import Awaitable, Callable, Generator, Mapping
from contextlib import contextmanager from contextlib import contextmanager
from enum import Enum from enum import Enum
from typing import Any, Final, Literal, cast from typing import Any, Final, Literal, Protocol, cast
from pydantic import BaseModel, ConfigDict, TypeAdapter from pydantic import BaseModel, ConfigDict, TypeAdapter
@ -25,6 +25,7 @@ from litellm._internal_context import current_service_target, service_target
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
from litellm.constants import CACHED_STREAMING_CHUNK_DELAY from litellm.constants import CACHED_STREAMING_CHUNK_DELAY
from litellm.integrations.otel.runtime import phase_span from litellm.integrations.otel.runtime import phase_span
from litellm.litellm_core_utils.core_helpers import require_attributes
from litellm.litellm_core_utils.model_param_helper import ( from litellm.litellm_core_utils.model_param_helper import (
ModelParamHelper, ModelParamHelper,
embeds_thought_signature_in_tool_call_id, embeds_thought_signature_in_tool_call_id,
@ -52,6 +53,16 @@ _REDIS_STARTUP_NODES_ADAPTER: Final = TypeAdapter(
) )
class _SupportsAsyncPing(Protocol):
@property
def ping(self) -> Callable[[], Awaitable[object]] | None: ...
class _SupportsAsyncDeleteCacheKeys(Protocol):
@property
def delete_cache_keys(self) -> Callable[[object], Awaitable[object]] | None: ...
def _keyed_provider_params_in_extra_body(kwargs: Mapping[str, object]) -> str: def _keyed_provider_params_in_extra_body(kwargs: Mapping[str, object]) -> str:
raw_extra_body: Final = kwargs.get("extra_body") raw_extra_body: Final = kwargs.get("extra_body")
if not isinstance(raw_extra_body, dict): if not isinstance(raw_extra_body, dict):
@ -1032,14 +1043,16 @@ class Cache:
cache_key, cached_data, kwargs = self._add_cache_logic(result=result, **kwargs) cache_key, cached_data, kwargs = self._add_cache_logic(result=result, **kwargs)
await self.cache.batch_cache_write(cache_key, cached_data, **kwargs) await self.cache.batch_cache_write(cache_key, cached_data, **kwargs)
async def ping(self): async def ping(self) -> object:
cache_ping: Final = getattr(self.cache, "ping") cache_ping: Final = require_attributes(self.cache, _SupportsAsyncPing, "ping").ping
if cache_ping: if cache_ping:
return await cache_ping() return await cache_ping()
return None return None
async def delete_cache_keys(self, keys): async def delete_cache_keys(self, keys: object) -> object:
cache_delete_cache_keys: Final = getattr(self.cache, "delete_cache_keys") cache_delete_cache_keys: Final = require_attributes(
self.cache, _SupportsAsyncDeleteCacheKeys, "delete_cache_keys"
).delete_cache_keys
if cache_delete_cache_keys: if cache_delete_cache_keys:
return await cache_delete_cache_keys(keys) return await cache_delete_cache_keys(keys)
return None return None

View file

@ -22,6 +22,7 @@ from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable,
from typing import TYPE_CHECKING, Any, Final, Optional, TypeVar from typing import TYPE_CHECKING, Any, Final, Optional, TypeVar
from pydantic import ConfigDict, SkipValidation, TypeAdapter, ValidationError from pydantic import ConfigDict, SkipValidation, TypeAdapter, ValidationError
from typing_extensions import TypedDict
import litellm import litellm
from litellm._internal_context import is_internal_call, post_response_phase from litellm._internal_context import is_internal_call, post_response_phase
@ -90,6 +91,10 @@ class CachingHandlerResponse(LiteLLMBaseModel):
in_memory_cache_obj: Final = InMemoryCache() in_memory_cache_obj: Final = InMemoryCache()
class _CacheCallKwargs(TypedDict, total=False, extra_items=object):
pass
def _drop_logging_obj_from_kwargs(request_kwargs: dict[str, object]) -> dict[str, object]: def _drop_logging_obj_from_kwargs(request_kwargs: dict[str, object]) -> dict[str, object]:
""" """
The caching handler is stored on the Logging object The caching handler is stored on the Logging object
@ -455,7 +460,7 @@ class LLMCachingHandler:
if litellm.cache is not None and self._is_call_type_supported_by_cache(original_function=original_function): if litellm.cache is not None and self._is_call_type_supported_by_cache(original_function=original_function):
args = args or () args = args or ()
# Now that we confirmed caching will happen, prepare kwargs # Now that we confirmed caching will happen, prepare kwargs
new_kwargs: Final = kwargs.copy() new_kwargs: Final = dict[str, object](kwargs)
new_kwargs.update( new_kwargs.update(
convert_args_to_kwargs( convert_args_to_kwargs(
self.original_function, self.original_function,
@ -470,7 +475,7 @@ class LLMCachingHandler:
print_verbose("Checking Sync Cache") print_verbose("Checking Sync Cache")
logging_obj.reset_caching_details() logging_obj.reset_caching_details()
with response_cache_phase("get"): with response_cache_phase("get"):
cached_result = litellm.cache.get_cache(**new_kwargs) cached_result = litellm.cache.get_cache(**_CacheCallKwargs(**new_kwargs))
if is_response_without_output(cached_result): if is_response_without_output(cached_result):
verbose_logger.debug("LiteLLM Cache: cached response has no output, treating it as a miss") verbose_logger.debug("LiteLLM Cache: cached response has no output, treating it as a miss")
return CachingHandlerResponse(cached_result=None) return CachingHandlerResponse(cached_result=None)
@ -480,7 +485,7 @@ class LLMCachingHandler:
pass pass
else: else:
call_type = original_function.__name__ call_type = original_function.__name__
cached_result = self._convert_cached_result_to_model_response( converted_result: Final = self._convert_cached_result_to_model_response(
cached_result=cached_result, cached_result=cached_result,
call_type=call_type, call_type=call_type,
kwargs=kwargs, kwargs=kwargs,
@ -508,16 +513,16 @@ class LLMCachingHandler:
logging_obj=logging_obj, logging_obj=logging_obj,
model=f"{custom_llm_provider}/{model}", model=f"{custom_llm_provider}/{model}",
kwargs=kwargs, kwargs=kwargs,
cached_result=cached_result, cached_result=converted_result,
is_async=False, is_async=False,
custom_llm_provider=custom_llm_provider, custom_llm_provider=custom_llm_provider,
) )
if not is_internal_call.get() and not _should_defer_streaming_cache_hit_callbacks( if not is_internal_call.get() and not _should_defer_streaming_cache_hit_callbacks(
cached_result=cached_result cached_result=converted_result
): ):
logging_obj.handle_sync_cache_hit_success_callbacks( logging_obj.handle_sync_cache_hit_success_callbacks(
result=cached_result, result=converted_result,
start_time=start_time, start_time=start_time,
end_time=end_time, end_time=end_time,
cache_hit=cache_hit, cache_hit=cache_hit,
@ -527,8 +532,8 @@ class LLMCachingHandler:
or self.request_kwargs.get("cache_key") or self.request_kwargs.get("cache_key")
or litellm.cache.get_cache_key(**self.request_kwargs) or litellm.cache.get_cache_key(**self.request_kwargs)
) )
_set_cached_hidden_param(cached_result, "cache_key", cache_key) _set_cached_hidden_param(converted_result, "cache_key", cache_key)
return CachingHandlerResponse(cached_result=cached_result) return CachingHandlerResponse(cached_result=converted_result)
return CachingHandlerResponse(cached_result=cached_result) return CachingHandlerResponse(cached_result=cached_result)
_sync_get_cache = sync_get_cache _sync_get_cache = sync_get_cache
@ -950,22 +955,14 @@ class LLMCachingHandler:
def _convert_cached_result_to_model_response( def _convert_cached_result_to_model_response(
self, self,
cached_result: Any, cached_result: object,
call_type: str, call_type: str,
kwargs: dict[str, object], kwargs: dict[str, object],
logging_obj: LiteLLMLoggingObj, logging_obj: LiteLLMLoggingObj,
model: str, model: str,
args: tuple[object, ...], args: tuple[object, ...],
custom_llm_provider: str | None = None, custom_llm_provider: str | None = None,
) -> ( ) -> object:
ModelResponse
| TextCompletionResponse
| EmbeddingResponse
| RerankResponse
| TranscriptionResponse
| CustomStreamWrapper
| None
):
""" """
Internal method to process the cached result Internal method to process the cached result
@ -1141,7 +1138,7 @@ class LLMCachingHandler:
self, self,
result: object, result: object,
original_function: Callable, original_function: Callable,
kwargs: dict[str, Any], kwargs: Mapping[str, object],
args: tuple[object, ...] | None = None, args: tuple[object, ...] | None = None,
): ):
""" """
@ -1169,7 +1166,7 @@ class LLMCachingHandler:
return return
cache: Final = litellm.cache cache: Final = litellm.cache
new_kwargs: Final = kwargs.copy() new_kwargs: Final = dict(kwargs)
new_kwargs.update( new_kwargs.update(
convert_args_to_kwargs( convert_args_to_kwargs(
original_function, original_function,
@ -1206,7 +1203,7 @@ class LLMCachingHandler:
) )
) )
else: else:
create_cache_write_task(lambda: cache.async_add_cache(result, **new_kwargs)) create_cache_write_task(lambda: cache.async_add_cache(result, **_CacheCallKwargs(**new_kwargs)))
def sync_set_cache( def sync_set_cache(
self, self,

View file

@ -40,7 +40,7 @@ from litellm.constants import (
REDIS_CIRCUIT_BREAKER_TIMEOUT_MIN_DURATION, REDIS_CIRCUIT_BREAKER_TIMEOUT_MIN_DURATION,
REDIS_TIMEOUT_LOG_INTERVAL, REDIS_TIMEOUT_LOG_INTERVAL,
) )
from litellm.litellm_core_utils.core_helpers import get_parent_otel_span_from_kwargs from litellm.litellm_core_utils.core_helpers import get_parent_otel_span_from_kwargs, has_attributes
from litellm.litellm_core_utils.coroutine_checker import coroutine_checker from litellm.litellm_core_utils.coroutine_checker import coroutine_checker
from litellm.redis.store_settings import RedisStoreSettings, breaker_settings from litellm.redis.store_settings import RedisStoreSettings, breaker_settings
from litellm.types.caching import ( from litellm.types.caching import (
@ -97,6 +97,22 @@ class _AsyncRedisCommands(Protocol):
def eval(self, script: str, numkeys: int, *keys_and_args: str | bytes | float) -> Awaitable[object]: ... def eval(self, script: str, numkeys: int, *keys_and_args: str | bytes | float) -> Awaitable[object]: ...
class _AsyncLuaScript(Protocol):
def __call__(
self, keys: Sequence[str], args: Sequence[str | bytes | int | float], client: object
) -> Awaitable[object]: ...
class _SupportsRegisterScript(Protocol):
def register_script(self, script: str, /) -> _AsyncLuaScript: ...
class _SupportsScriptLoad(Protocol):
def script_load(self, script: str, /) -> object: ...
def evalsha(self, sha: object, numkeys: int, /, *keys_and_args: str | bytes | float) -> Awaitable[object]: ...
_GENERIC_CALLER_MODULES: Final = frozenset( _GENERIC_CALLER_MODULES: Final = frozenset(
{ {
__name__, __name__,
@ -844,6 +860,9 @@ class RedisCache(BaseCache):
def _async_commands(self) -> _AsyncRedisCommands: def _async_commands(self) -> _AsyncRedisCommands:
return self.init_async_client() return self.init_async_client()
def _async_client_object(self) -> object:
return self.init_async_client()
def check_and_fix_namespace(self, key: str) -> str: def check_and_fix_namespace(self, key: str) -> str:
""" """
Make sure each key starts with the given namespace Make sure each key starts with the given namespace
@ -1117,8 +1136,8 @@ class RedisCache(BaseCache):
Kept separate from async_register_script so each loop caches its own Kept separate from async_register_script so each loop caches its own
executor; see that method for why the binding must be per loop. executor; see that method for why the binding must be per loop.
""" """
_redis_client: Final[Any] = self.init_async_client() _redis_client: Final = self._async_client_object()
if hasattr(_redis_client, "register_script"): if has_attributes(_redis_client, _SupportsRegisterScript, "register_script"):
registered_script: Final = _redis_client.register_script(script) registered_script: Final = _redis_client.register_script(script)
async def standalone_executor( async def standalone_executor(
@ -1131,7 +1150,7 @@ class RedisCache(BaseCache):
return standalone_executor return standalone_executor
if hasattr(_redis_client, "script_load"): if has_attributes(_redis_client, _SupportsScriptLoad, "script_load"):
script_sha: Final = _redis_client.script_load(script) script_sha: Final = _redis_client.script_load(script)
async def cluster_executor( async def cluster_executor(

View file

@ -43,6 +43,7 @@ if TYPE_CHECKING:
_CACHE_RESPONSE_ADAPTER: Final[TypeAdapter[str | bytes | None]] = TypeAdapter( _CACHE_RESPONSE_ADAPTER: Final[TypeAdapter[str | bytes | None]] = TypeAdapter(
str | bytes | None, config=ConfigDict(strict=True, hide_input_in_errors=True) str | bytes | None, config=ConfigDict(strict=True, hide_input_in_errors=True)
) )
_DECODED_CACHE_VALUE: Final = TypeAdapter(object)
class RedisSemanticCache(BaseCache): class RedisSemanticCache(BaseCache):
@ -407,10 +408,10 @@ class RedisSemanticCache(BaseCache):
# Convert string representation to Python object # Convert string representation to Python object
try: try:
return json.loads(response_text) return _DECODED_CACHE_VALUE.validate_python(json.loads(response_text))
except json.JSONDecodeError: except json.JSONDecodeError:
try: try:
return ast.literal_eval(response_text) return _DECODED_CACHE_VALUE.validate_python(ast.literal_eval(response_text))
except (ValueError, SyntaxError) as e: except (ValueError, SyntaxError) as e:
print_verbose(f"Error parsing cached response: {e}") print_verbose(f"Error parsing cached response: {e}")
return None return None

View file

@ -39,7 +39,7 @@ from litellm.litellm_core_utils.allowed_tools_tool_choice import (
parse_chat_allowed_tools_tool_choice, parse_chat_allowed_tools_tool_choice,
responses_allowed_tools_tool_choice, responses_allowed_tools_tool_choice,
) )
from litellm.litellm_core_utils.core_helpers import drops_unsupported_params from litellm.litellm_core_utils.core_helpers import drops_unsupported_params, iter_items
from litellm.litellm_core_utils.hidden_params import get_hidden_params, get_or_create_hidden_params from litellm.litellm_core_utils.hidden_params import get_hidden_params, get_or_create_hidden_params
from litellm.litellm_core_utils.prompt_templates.prompt_cache_breakpoint_utils import ( from litellm.litellm_core_utils.prompt_templates.prompt_cache_breakpoint_utils import (
prompt_cache_breakpoint_with_readable_mode, prompt_cache_breakpoint_with_readable_mode,
@ -349,6 +349,9 @@ _TOOL_SEARCH_REPLAY_ITEM_TYPES: Final = frozenset({"reasoning", *_TOOL_SEARCH_IT
_TOOL_SEARCH_ITEMS_KEY: Final = "tool_search_items" _TOOL_SEARCH_ITEMS_KEY: Final = "tool_search_items"
_DECODED_JSON: Final = TypeAdapter(object)
def _json_field(value: object, key: str) -> object: def _json_field(value: object, key: str) -> object:
return cast(Mapping[str, object], value).get(key) if isinstance(value, dict) else None # cast-ok: untyped JSON return cast(Mapping[str, object], value).get(key) if isinstance(value, dict) else None # cast-ok: untyped JSON
@ -857,8 +860,8 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
# Handle message items with output_text content # Handle message items with output_text content
if item_type == "message": if item_type == "message":
content_list: Final = item.get("content", []) content_list: Final = _DECODED_JSON.validate_python(item.get("content", []))
for content_item in content_list: for content_item in iter_items(content_list):
if isinstance(content_item, dict): if isinstance(content_item, dict):
content_type = content_item.get("type") content_type = content_item.get("type")
if content_type == "output_text": if content_type == "output_text":

View file

@ -8,7 +8,7 @@ from datetime import datetime
from functools import lru_cache from functools import lru_cache
from itertools import accumulate from itertools import accumulate
from types import MappingProxyType from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Final, Literal, TypeAlias, cast from typing import TYPE_CHECKING, Any, Final, Literal, Protocol, TypeAlias, cast
from httpx import Response from httpx import Response
from pydantic import BaseModel, ConfigDict, TypeAdapter, ValidationError from pydantic import BaseModel, ConfigDict, TypeAdapter, ValidationError
@ -24,7 +24,12 @@ from litellm.constants import (
REALTIME_METERED_USAGE_KEY, REALTIME_METERED_USAGE_KEY,
) )
from litellm.litellm_core_utils.audio_utils.utils import normalized_audio_duration_seconds from litellm.litellm_core_utils.audio_utils.utils import normalized_audio_duration_seconds
from litellm.litellm_core_utils.core_helpers import as_str_mapping, attribute_or_none, billed_usage_cost from litellm.litellm_core_utils.core_helpers import (
as_str_mapping,
attribute_or_none,
billed_usage_cost,
require_attributes,
)
from litellm.litellm_core_utils.cost_discount import resolve_cost_discount from litellm.litellm_core_utils.cost_discount import resolve_cost_discount
from litellm.litellm_core_utils.decisions_pricing import ( from litellm.litellm_core_utils.decisions_pricing import (
decisions_pricing_fields, decisions_pricing_fields,
@ -1188,6 +1193,10 @@ def _model_contains_known_llm_provider(model: str) -> bool:
return _provider_prefix in LlmProvidersSet return _provider_prefix in LlmProvidersSet
class _SupportsGet(Protocol):
def get(self, key: str, /) -> object: ...
def _get_response_model(completion_response: object) -> str | None: def _get_response_model(completion_response: object) -> str | None:
""" """
Extract the model name from a completion response object. Extract the model name from a completion response object.
@ -1219,7 +1228,7 @@ _GEMINI_TRAFFIC_TYPE_TO_SERVICE_TIER: Final[dict] = {
} }
def _map_traffic_type_to_service_tier(traffic_type: str | None) -> str | None: def _map_traffic_type_to_service_tier(traffic_type: object) -> str | None:
""" """
Map a Gemini usageMetadata.trafficType value to a LiteLLM service_tier string. Map a Gemini usageMetadata.trafficType value to a LiteLLM service_tier string.
@ -1273,11 +1282,13 @@ def _served_service_tier(completion_response: object, usage_object: Usage | None
usage_tier: Final = _extract_service_tier(usage_object) usage_tier: Final = _extract_service_tier(usage_object)
if isinstance(usage_tier, str): if isinstance(usage_tier, str):
return usage_tier return usage_tier
hidden_params: Final = getattr(completion_response, "_hidden_params", None) hidden_params: Final[object] = getattr(completion_response, "_hidden_params", None)
if hidden_params is None: if hidden_params is None:
return None return None
provider_specific: Final = hidden_params.get("provider_specific_fields") or {} provider_specific: Final[object] = (
raw_traffic_type: Final = provider_specific.get("traffic_type") require_attributes(hidden_params, _SupportsGet, "get").get("provider_specific_fields") or {}
)
raw_traffic_type: Final = require_attributes(provider_specific, _SupportsGet, "get").get("traffic_type")
if not raw_traffic_type: if not raw_traffic_type:
return None return None
return _map_traffic_type_to_service_tier(raw_traffic_type) or "default" return _map_traffic_type_to_service_tier(raw_traffic_type) or "default"
@ -1341,8 +1352,16 @@ def get_usage_object(
return None return None
class _DumpedUsageFields(TypedDict, total=False, extra_items=object):
pass
def _dumped_usage_fields(usage_model: BaseModel) -> Mapping[str, object]:
return usage_model.model_dump()
def _usage_from_model(usage_model: BaseModel) -> Usage: def _usage_from_model(usage_model: BaseModel) -> Usage:
return Usage(**usage_model.model_dump()) return Usage(**_DumpedUsageFields(**_dumped_usage_fields(usage_model)))
def _decisions_prompt_tokens_details(usage: DecisionsUsage | OpenAIDecisionUsage) -> PromptTokensDetailsWrapper: def _decisions_prompt_tokens_details(usage: DecisionsUsage | OpenAIDecisionUsage) -> PromptTokensDetailsWrapper:

View file

@ -12,10 +12,11 @@
import datetime import datetime
import enum import enum
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from typing import Any, Final from typing import Any, Final, Protocol, SupportsIndex, SupportsInt, runtime_checkable
import httpx import httpx
import openai import openai
from typing_extensions import Buffer
import litellm import litellm
from litellm.types.utils import LiteLLMCommonStrings from litellm.types.utils import LiteLLMCommonStrings
@ -1250,6 +1251,19 @@ class BlockedPiiEntityError(Exception):
super().__init__(self.message) super().__init__(self.message)
@runtime_checkable
class _SupportsTrunc(Protocol):
def __trunc__(self) -> int: ...
def _status_code_int(value: object) -> int:
if isinstance(value, (str, Buffer, SupportsInt, SupportsIndex, _SupportsTrunc)):
return int(value)
raise TypeError(
f"int() argument must be a string, a bytes-like object or a real number, not '{type(value).__name__}'"
)
class MidStreamFallbackError(ServiceUnavailableError): class MidStreamFallbackError(ServiceUnavailableError):
def __init__( def __init__(
self, self,
@ -1265,8 +1279,8 @@ class MidStreamFallbackError(ServiceUnavailableError):
is_pre_first_chunk: bool = False, is_pre_first_chunk: bool = False,
emitted_disqualifying_content: bool = False, emitted_disqualifying_content: bool = False,
): ):
original_status: Final = getattr(original_exception, "status_code", None) original_status: Final[object] = getattr(original_exception, "status_code", None)
self.status_code = int(original_status) if original_status is not None else 503 self.status_code = _status_code_int(original_status) if original_status is not None else 503
self.message = f"litellm.MidStreamFallbackError: {message}" self.message = f"litellm.MidStreamFallbackError: {message}"
self.model = model self.model = model
self.llm_provider = llm_provider self.llm_provider = llm_provider
@ -1309,7 +1323,7 @@ class MidStreamFallbackError(ServiceUnavailableError):
) )
# Restore the propagated status and original response/request objects # Restore the propagated status and original response/request objects
self.status_code = int(original_status) if original_status is not None else 503 self.status_code = _status_code_int(original_status) if original_status is not None else 503
self.response = _saved_response self.response = _saved_response
self.request = _saved_request self.request = _saved_request
self.message = _saved_message self.message = _saved_message

View file

@ -14,10 +14,11 @@ import contextlib
import itertools import itertools
import json import json
import logging import logging
import math
import secrets import secrets
from collections.abc import AsyncIterable, AsyncIterator, Mapping from collections.abc import AsyncIterable, AsyncIterator, Callable, Mapping
from dataclasses import dataclass from dataclasses import dataclass
from types import MappingProxyType, ModuleType from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Final, Protocol from typing import TYPE_CHECKING, Any, Final, Protocol
import httpx import httpx
@ -36,6 +37,7 @@ from litellm.constants import (
from litellm.harness.context import GatewayTarget from litellm.harness.context import GatewayTarget
from litellm.harness.errors import HarnessError, HarnessInstallFailed from litellm.harness.errors import HarnessError, HarnessInstallFailed
from litellm.harness.types import Harness, Usage from litellm.harness.types import Harness, Usage
from litellm.litellm_core_utils.core_helpers import has_attributes
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
from litellm.types.litellm_params import STREAM_USAGE_INJECTED_FIELD from litellm.types.litellm_params import STREAM_USAGE_INJECTED_FIELD
from litellm.types.llms.custom_http import httpxSpecialProvider from litellm.types.llms.custom_http import httpxSpecialProvider
@ -45,7 +47,7 @@ if TYPE_CHECKING:
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import JSONResponse, Response, StreamingResponse from starlette.responses import JSONResponse, Response, StreamingResponse
from starlette.routing import Route from starlette.routing import Route
from uvicorn import Server from uvicorn import Config, Server
verbose_logger: Final = logging.getLogger("LiteLLM") verbose_logger: Final = logging.getLogger("LiteLLM")
_JSON_VALUE_ADAPTER: Final = TypeAdapter(object, config=ConfigDict(strict=True)) _JSON_VALUE_ADAPTER: Final = TypeAdapter(object, config=ConfigDict(strict=True))
@ -110,9 +112,22 @@ class _ResponsesModule(Protocol):
StreamingResponse: type[StreamingResponse] StreamingResponse: type[StreamingResponse]
class _UvicornModule(Protocol):
Config: type[Config]
Server: type[Server]
class _CapturesSignals(Protocol):
capture_signals: Callable[[], contextlib.AbstractContextManager[None]]
class _InstallsSignalHandlers(Protocol):
install_signal_handlers: Callable[[], None]
@dataclass(frozen=True) @dataclass(frozen=True)
class _ServerDeps: class _ServerDeps:
uvicorn: ModuleType uvicorn: _UvicornModule
applications: _ApplicationsModule applications: _ApplicationsModule
routing: _RoutingModule routing: _RoutingModule
responses: _ResponsesModule responses: _ResponsesModule
@ -438,6 +453,14 @@ def _noop() -> None:
return None return None
def _signal_capture(server: object) -> _CapturesSignals | None:
return server if has_attributes(server, _CapturesSignals, "capture_signals") else None
def _signal_handler_installer(server: object) -> _InstallsSignalHandlers | None:
return server if has_attributes(server, _InstallsSignalHandlers, "install_signal_handlers") else None
class ModelEndpoint: class ModelEndpoint:
"""Local HTTP endpoint for one harness session. Use as an async context manager.""" """Local HTTP endpoint for one harness session. Use as an async context manager."""
@ -521,7 +544,7 @@ class ModelEndpoint:
await asyncio.sleep(DEFAULT_POLLING_INTERVAL) await asyncio.sleep(DEFAULT_POLLING_INTERVAL)
def _build_server(self, deps: _ServerDeps) -> Server: def _build_server(self, deps: _ServerDeps) -> Server:
config = deps.uvicorn.Config( config: Final = deps.uvicorn.Config(
self._build_app(deps), self._build_app(deps),
host=HARNESS_ENDPOINT_HOST, host=HARNESS_ENDPOINT_HOST,
port=0, port=0,
@ -529,14 +552,16 @@ class ModelEndpoint:
log_level="warning", log_level="warning",
access_log=False, access_log=False,
lifespan="off", lifespan="off",
timeout_graceful_shutdown=HARNESS_PROCESS_KILL_GRACE_SECONDS, timeout_graceful_shutdown=math.ceil(HARNESS_PROCESS_KILL_GRACE_SECONDS),
) )
server = deps.uvicorn.Server(config) server: Final = deps.uvicorn.Server(config)
# Never touch the host process's signal handlers. # Never touch the host process's signal handlers.
if hasattr(server, "capture_signals"): signal_capture: Final = _signal_capture(server)
server.capture_signals = contextlib.nullcontext if signal_capture is not None:
if hasattr(server, "install_signal_handlers"): signal_capture.capture_signals = contextlib.nullcontext
server.install_signal_handlers = _noop handler_installer: Final = _signal_handler_installer(server)
if handler_installer is not None:
handler_installer.install_signal_handlers = _noop
return server return server
def _build_app(self, deps: _ServerDeps) -> Starlette: def _build_app(self, deps: _ServerDeps) -> Starlette:

View file

@ -6,12 +6,25 @@ from pathlib import Path
from typing import Any, Final, Protocol, cast, get_type_hints, runtime_checkable from typing import Any, Final, Protocol, cast, get_type_hints, runtime_checkable
import litellm import litellm
from litellm.litellm_core_utils.core_helpers import has_attributes
from litellm.litellm_core_utils.token_counter import get_image_type from litellm.litellm_core_utils.token_counter import get_image_type
from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig
from litellm.types.files import FILE_MIME_TYPES, FileType from litellm.types.files import FILE_MIME_TYPES, FileType
from litellm.types.images.main import ImageEditOptionalRequestParams from litellm.types.images.main import ImageEditOptionalRequestParams
class _ReadsBytes(Protocol):
def read(self, size: int, /) -> bytes: ...
class _Tells(Protocol):
def tell(self) -> int: ...
class _Seeks(Protocol):
def seek(self, offset: int, /) -> object: ...
class ImageEditRequestUtils: class ImageEditRequestUtils:
@staticmethod @staticmethod
def get_optional_params_image_edit( def get_optional_params_image_edit(
@ -114,12 +127,12 @@ class ImageEditRequestUtils:
bytes_data = image_data[:100] bytes_data = image_data[:100]
else: else:
# For other types, try to read if possible # For other types, try to read if possible
if hasattr(image_data, "read"): if has_attributes(image_data, _ReadsBytes, "read"):
current_pos = getattr(image_data, "tell", lambda: 0)() current_pos = image_data.tell() if has_attributes(image_data, _Tells, "tell") else 0
if hasattr(image_data, "seek"): if has_attributes(image_data, _Seeks, "seek"):
image_data.seek(0) image_data.seek(0)
bytes_data = image_data.read(100) bytes_data = image_data.read(100)
if hasattr(image_data, "seek"): if has_attributes(image_data, _Seeks, "seek"):
image_data.seek(current_pos) image_data.seek(current_pos)
else: else:
return FILE_MIME_TYPES[FileType.PNG] # Default fallback return FILE_MIME_TYPES[FileType.PNG] # Default fallback

View file

@ -53,11 +53,13 @@ async def fetch_user_spend_rows(
today: datetime.date, today: datetime.date,
baseline_days: int, baseline_days: int,
) -> tuple[UserSpendRow, ...]: ) -> tuple[UserSpendRow, ...]:
from litellm.proxy.utils import query_raw_rows
today_str: Final = today.strftime("%Y-%m-%d") today_str: Final = today.strftime("%Y-%m-%d")
month_start_str: Final = today.replace(day=1).strftime("%Y-%m-%d") month_start_str: Final = today.replace(day=1).strftime("%Y-%m-%d")
baseline_start_str: Final = (today - datetime.timedelta(days=max(baseline_days, 1))).strftime("%Y-%m-%d") baseline_start_str: Final = (today - datetime.timedelta(days=max(baseline_days, 1))).strftime("%Y-%m-%d")
return USER_SPEND_ROWS_ADAPTER.validate_python( return USER_SPEND_ROWS_ADAPTER.validate_python(
await prisma_client.db.query_raw(USER_SPEND_QUERY, today_str, month_start_str, baseline_start_str) await query_raw_rows(prisma_client.db, USER_SPEND_QUERY, today_str, month_start_str, baseline_start_str)
) )

View file

@ -7,11 +7,12 @@ import json
import os import os
import random import random
import types import types
from collections.abc import Mapping from collections.abc import Mapping, Sequence
from typing import Final from typing import Final, Protocol
import httpx import httpx
from pydantic import BaseModel from pydantic import BaseModel
from typing_extensions import ReadOnly, TypedDict
import litellm import litellm
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
@ -29,6 +30,22 @@ from litellm.types.integrations.argilla import (
from litellm.types.utils import StandardLoggingPayload from litellm.types.utils import StandardLoggingPayload
class _ArgillaDataset(TypedDict):
id: ReadOnly[str]
class _ArgillaDatasetsBody(TypedDict, total=False):
items: ReadOnly[Sequence[_ArgillaDataset]]
class _ArgillaDatasetsResponse(Protocol):
def json(self) -> _ArgillaDatasetsBody: ...
def _datasets_body(response: _ArgillaDatasetsResponse) -> _ArgillaDatasetsBody:
return response.json()
def is_serializable(value): def is_serializable(value):
non_serializable_types: Final = ( non_serializable_types: Final = (
types.CoroutineType, types.CoroutineType,
@ -102,7 +119,7 @@ class ArgillaLogger(CustomBatchLogger):
url=f"{_credentials_base_url}/api/v1/me/datasets?name={_credentials_dataset_name}", url=f"{_credentials_base_url}/api/v1/me/datasets?name={_credentials_dataset_name}",
headers={"X-Argilla-Api-Key": _credentials_api_key}, headers={"X-Argilla-Api-Key": _credentials_api_key},
) )
json_response: Final = dataset_response.json() json_response: Final = _datasets_body(dataset_response)
if ( if (
"items" in json_response "items" in json_response
and isinstance(json_response["items"], list) and isinstance(json_response["items"], list)

View file

@ -1,6 +1,6 @@
import json import json
from collections.abc import Mapping from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, Final from typing import TYPE_CHECKING, Final, Protocol
from pydantic import ConfigDict, TypeAdapter from pydantic import ConfigDict, TypeAdapter
from typing_extensions import ReadOnly, TypedDict, override from typing_extensions import ReadOnly, TypedDict, override
@ -10,6 +10,7 @@ from litellm.integrations.opentelemetry_utils.base_otel_llm_obs_attributes impor
BaseLLMObsOTELAttributes, BaseLLMObsOTELAttributes,
safe_set_attribute, safe_set_attribute,
) )
from litellm.litellm_core_utils.core_helpers import iter_items, require_attributes, subscript
from litellm.litellm_core_utils.redact_messages import ( from litellm.litellm_core_utils.redact_messages import (
should_redact_message_logging, should_redact_message_logging,
) )
@ -34,32 +35,40 @@ from litellm.integrations._types.open_inference import (
_JSON_OBJECT: Final = TypeAdapter(dict[str, object], config=ConfigDict(hide_input_in_errors=True)) _JSON_OBJECT: Final = TypeAdapter(dict[str, object], config=ConfigDict(hide_input_in_errors=True))
class _SupportsGetWithDefault(Protocol):
def get(self, key: str, default: object = None, /) -> object: ...
class ArizeOTELAttributes(BaseLLMObsOTELAttributes): class ArizeOTELAttributes(BaseLLMObsOTELAttributes):
@staticmethod @staticmethod
@override @override
def set_messages(span: "Span", kwargs: dict[str, Any]): def set_messages(span: "Span", kwargs: Mapping[str, object]):
messages: Final = kwargs.get("messages") messages: Final = kwargs.get("messages")
# for /chat/completions # for /chat/completions
# https://docs.arize.com/arize/large-language-models/tracing/semantic-conventions # https://docs.arize.com/arize/large-language-models/tracing/semantic-conventions
if messages: if messages:
last_message: Final = messages[-1] last_message: Final = subscript(messages, -1)
safe_set_attribute( safe_set_attribute(
span, span,
SpanAttributes.INPUT_VALUE, SpanAttributes.INPUT_VALUE,
last_message.get("content", ""), require_attributes(last_message, _SupportsGetWithDefault, "get").get("content", ""),
) )
# LLM_INPUT_MESSAGES shows up under `input_messages` tab on the span page. # LLM_INPUT_MESSAGES shows up under `input_messages` tab on the span page.
for idx, msg in enumerate(messages): for idx, msg in enumerate(iter_items(messages)):
prefix = f"{SpanAttributes.LLM_INPUT_MESSAGES}.{idx}" prefix = f"{SpanAttributes.LLM_INPUT_MESSAGES}.{idx}"
# Set the role per message. # Set the role per message.
safe_set_attribute(span, f"{prefix}.{MessageAttributes.MESSAGE_ROLE}", msg.get("role")) safe_set_attribute(
span,
f"{prefix}.{MessageAttributes.MESSAGE_ROLE}",
require_attributes(msg, _SupportsGetWithDefault, "get").get("role"),
)
# Set the content per message. # Set the content per message.
safe_set_attribute( safe_set_attribute(
span, span,
f"{prefix}.{MessageAttributes.MESSAGE_CONTENT}", f"{prefix}.{MessageAttributes.MESSAGE_CONTENT}",
msg.get("content", ""), require_attributes(msg, _SupportsGetWithDefault, "get").get("content", ""),
) )
# Additive: emit structured tool_calls / multimodal content # Additive: emit structured tool_calls / multimodal content

View file

@ -18,10 +18,11 @@ import time
import traceback import traceback
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from types import MappingProxyType from types import MappingProxyType
from typing import Final, TypeVar from typing import Final, Protocol, TypeVar
from urllib.parse import urlparse from urllib.parse import urlparse
import httpx import httpx
from typing_extensions import ReadOnly, TypedDict
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
from litellm.integrations.batch_utils import ( from litellm.integrations.batch_utils import (
@ -52,6 +53,19 @@ MONITOR_SCOPE_BY_AUTHORITY_HOST: Final[Mapping[str, str]] = MappingProxyType(
) )
class _OAuthTokenBody(TypedDict, total=False):
access_token: ReadOnly[str]
expires_in: ReadOnly[int]
class _OAuthTokenResponse(Protocol):
def json(self) -> _OAuthTokenBody: ...
def _oauth_token_body(response: _OAuthTokenResponse) -> _OAuthTokenBody:
return response.json()
class AzureSentinelLogger(CustomBatchLogger): class AzureSentinelLogger(CustomBatchLogger):
""" """
Logger that sends LiteLLM logs to Azure Sentinel via Azure Monitor Logs Ingestion API Logger that sends LiteLLM logs to Azure Sentinel via Azure Monitor Logs Ingestion API
@ -226,7 +240,7 @@ class AzureSentinelLogger(CustomBatchLogger):
if response.status_code != 200: if response.status_code != 200:
raise Exception(f"Failed to get OAuth2 token: {response.status_code} - {response.text}") raise Exception(f"Failed to get OAuth2 token: {response.status_code} - {response.text}")
token_response: Final = response.json() token_response: Final = _oauth_token_body(response)
self.oauth_token = token_response.get("access_token") self.oauth_token = token_response.get("access_token")
expires_in: Final = token_response.get("expires_in", 3600) expires_in: Final = token_response.get("expires_in", 3600)

View file

@ -5,7 +5,7 @@ BitBucket API client for fetching .prompt files from BitBucket repositories.
import base64 import base64
import urllib.parse import urllib.parse
from collections.abc import Mapping from collections.abc import Mapping
from typing import Final, TypedDict from typing import Final, Protocol, TypedDict
from typing_extensions import NotRequired, ReadOnly from typing_extensions import NotRequired, ReadOnly
@ -36,6 +36,30 @@ class BitBucketFileMetadata(TypedDict):
last_modified: ReadOnly[str | None] last_modified: ReadOnly[str | None]
class _SrcListingResponse(Protocol):
def json(self) -> BitBucketSrcListing: ...
class _RepositoryInfoResponse(Protocol):
def json(self) -> Mapping[str, object]: ...
class _BranchListingResponse(Protocol):
def json(self) -> BitBucketBranchListing: ...
def _src_listing(response: _SrcListingResponse) -> BitBucketSrcListing:
return response.json()
def _repository_info(response: _RepositoryInfoResponse) -> Mapping[str, object]:
return response.json()
def _branch_listing(response: _BranchListingResponse) -> BitBucketBranchListing:
return response.json()
def _sanitize_file_path(file_path: str) -> str: def _sanitize_file_path(file_path: str) -> str:
"""Reject path traversal and URL-encode each path segment.""" """Reject path traversal and URL-encode each path segment."""
if "#" in file_path or "?" in file_path: if "#" in file_path or "?" in file_path:
@ -166,7 +190,7 @@ class BitBucketClient:
response: Final = self.http_handler.get(url, headers=self.headers) response: Final = self.http_handler.get(url, headers=self.headers)
response.raise_for_status() response.raise_for_status()
data: Final[BitBucketSrcListing] = response.json() data: Final = _src_listing(response)
return [ return [
file_path file_path
for item in data.get("values", []) for item in data.get("values", [])
@ -201,7 +225,7 @@ class BitBucketClient:
try: try:
response: Final = self.http_handler.get(url, headers=self.headers) response: Final = self.http_handler.get(url, headers=self.headers)
response.raise_for_status() response.raise_for_status()
return response.json() return _repository_info(response)
except Exception as e: except Exception as e:
raise Exception(f"Failed to get repository info: {e}") raise Exception(f"Failed to get repository info: {e}")
@ -231,7 +255,7 @@ class BitBucketClient:
response: Final = self.http_handler.get(url, headers=self.headers) response: Final = self.http_handler.get(url, headers=self.headers)
response.raise_for_status() response.raise_for_status()
data: Final[BitBucketBranchListing] = response.json() data: Final = _branch_listing(response)
return data.get("values", []) return data.get("values", [])
except Exception as e: except Exception as e:
raise Exception(f"Failed to get branches: {e}") raise Exception(f"Failed to get branches: {e}")

View file

@ -4,12 +4,13 @@ Fetches .prompt files from BitBucket repositories and provides team-based access
""" """
from collections.abc import Mapping from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, Final from typing import TYPE_CHECKING, Any, Final, Protocol
from jinja2 import DictLoader, select_autoescape from jinja2 import DictLoader, select_autoescape
from jinja2.sandbox import ImmutableSandboxedEnvironment from jinja2.sandbox import ImmutableSandboxedEnvironment
from litellm.integrations.custom_prompt_management import CustomPromptManagement from litellm.integrations.custom_prompt_management import CustomPromptManagement
from litellm.litellm_core_utils.core_helpers import require_attributes
if TYPE_CHECKING: if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
@ -26,6 +27,10 @@ from litellm.types.utils import StandardCallbackDynamicParams
from .bitbucket_client import BitBucketClient from .bitbucket_client import BitBucketClient
class _SupportsGetWithDefault(Protocol):
def get(self, key: str, default: object = None, /) -> object: ...
class BitBucketPromptTemplate: class BitBucketPromptTemplate:
""" """
Represents a prompt template loaded from BitBucket. Represents a prompt template loaded from BitBucket.
@ -35,7 +40,7 @@ class BitBucketPromptTemplate:
self, self,
template_id: str, template_id: str,
content: str, content: str,
metadata: dict[str, Any], metadata: Mapping[str, object],
model: str | None = None, model: str | None = None,
): ):
self.template_id = template_id self.template_id = template_id
@ -44,7 +49,9 @@ class BitBucketPromptTemplate:
self.model = model or metadata.get("model") self.model = model or metadata.get("model")
self.temperature = metadata.get("temperature") self.temperature = metadata.get("temperature")
self.max_tokens = metadata.get("max_tokens") self.max_tokens = metadata.get("max_tokens")
self.input_schema = metadata.get("input", {}).get("schema", {}) self.input_schema = require_attributes(metadata.get("input", {}), _SupportsGetWithDefault, "get").get(
"schema", {}
)
self.optional_params = {k: v for k, v in metadata.items() if k not in ["model", "input", "content"]} self.optional_params = {k: v for k, v in metadata.items() if k not in ["model", "input", "content"]}
def __repr__(self): def __repr__(self):

View file

@ -7,10 +7,10 @@ so `response_id` is always the raw provider response id (cache-hit suffix stripp
import json import json
import re import re
from collections.abc import Iterator, Mapping from collections.abc import Iterable, Iterator, Mapping
from math import isfinite from math import isfinite
from types import MappingProxyType from types import MappingProxyType
from typing import Any, Final from typing import Any, Final, Protocol
from pydantic import JsonValue, TypeAdapter, ValidationError from pydantic import JsonValue, TypeAdapter, ValidationError
@ -19,6 +19,7 @@ from litellm._logging import verbose_logger
from litellm.integrations.clickhouse.clickhouse_batch_logger import ClickHouseBatchLogger from litellm.integrations.clickhouse.clickhouse_batch_logger import ClickHouseBatchLogger
from litellm.integrations.clickhouse.context import is_lens_analysis from litellm.integrations.clickhouse.context import is_lens_analysis
from litellm.integrations.clickhouse.schema import SPEND_LOGS_TABLE from litellm.integrations.clickhouse.schema import SPEND_LOGS_TABLE
from litellm.litellm_core_utils.core_helpers import require_attributes
from litellm.litellm_core_utils.internal_call_metadata import logged_event_key from litellm.litellm_core_utils.internal_call_metadata import logged_event_key
from litellm.litellm_core_utils.llm_response_utils.get_headers import get_provider_request_id from litellm.litellm_core_utils.llm_response_utils.get_headers import get_provider_request_id
from litellm.litellm_core_utils.sensitive_data_masker import redact_credentials_in_payload from litellm.litellm_core_utils.sensitive_data_masker import redact_credentials_in_payload
@ -40,6 +41,14 @@ _INTERNAL_METADATA_KEYS: Final = frozenset(
) )
class _SupportsGet(Protocol):
def get(self, key: str, /) -> object: ...
class _SupportsItems(Protocol):
def items(self) -> Iterable[tuple[object, object]]: ...
def _request_metadata_fields(value: object) -> Iterator[tuple[str, JsonValue]]: def _request_metadata_fields(value: object) -> Iterator[tuple[str, JsonValue]]:
if value is None: if value is None:
return return
@ -85,22 +94,22 @@ def _json_mapping(value: Mapping[str, object]) -> str:
return _json(dict(value)) return _json(dict(value))
def _find_traceparent(metadata: Mapping[str, Any], kwargs: Mapping[str, Any]) -> tuple[str, str]: def _find_traceparent(metadata: Mapping[str, object], kwargs: Mapping[str, object]) -> tuple[str, str]:
custom_headers = metadata.get("requester_custom_headers") or MappingProxyType({}) custom_headers = metadata.get("requester_custom_headers") or MappingProxyType({})
proxy_request = (kwargs.get("litellm_params") or MappingProxyType({})).get( proxy_request = require_attributes(kwargs.get("litellm_params") or MappingProxyType({}), _SupportsGet, "get").get(
"proxy_server_request" "proxy_server_request"
) or MappingProxyType({}) ) or MappingProxyType({})
request_headers = proxy_request.get("headers") or MappingProxyType({}) request_headers = require_attributes(proxy_request, _SupportsGet, "get").get("headers") or MappingProxyType({})
for headers in (custom_headers, request_headers): for headers in (custom_headers, request_headers):
for name, value in headers.items(): for name, value in require_attributes(headers, _SupportsItems, "items").items():
if str(name).lower() == "traceparent": if str(name).lower() == "traceparent":
return parse_traceparent(value) return parse_traceparent(value)
return "", "" return "", ""
def _cache_tokens(usage: Mapping[str, Any]) -> tuple[int, int]: def _cache_tokens(usage: Mapping[str, object]) -> tuple[int, int]:
"""(cache_read, cache_write) from a Usage dict: OpenAI prompt_tokens_details first, Anthropic fields as fallback.""" """(cache_read, cache_write) from a Usage dict: OpenAI prompt_tokens_details first, Anthropic fields as fallback."""
details = usage.get("prompt_tokens_details") or MappingProxyType({}) details = require_attributes(usage.get("prompt_tokens_details") or MappingProxyType({}), _SupportsGet, "get")
cache_read = _int(details.get("cached_tokens")) or _int(usage.get("cache_read_input_tokens")) cache_read = _int(details.get("cached_tokens")) or _int(usage.get("cache_read_input_tokens"))
cache_write = ( cache_write = (
_int(details.get("cache_write_tokens")) _int(details.get("cache_write_tokens"))
@ -116,10 +125,17 @@ def _request_tags(value: object) -> list[str]:
return [str(tag) for tag in value] return [str(tag) for tag in value]
def _session_id(payload: StandardLoggingPayload | SpendLogPayload, kwargs: Mapping[str, Any]) -> str: def _session_id(payload: StandardLoggingPayload | SpendLogPayload, kwargs: Mapping[str, object]) -> str:
"""Mirrors proxy `_get_session_id_for_spend_log`: explicit session id, else the payload trace id.""" """Mirrors proxy `_get_session_id_for_spend_log`: explicit session id, else the payload trace id."""
request_metadata = (kwargs.get("litellm_params") or MappingProxyType({})).get("metadata") or MappingProxyType({}) request_metadata = require_attributes(
return str(payload.get("session_id") or request_metadata.get("session_id") or payload.get("trace_id") or "") kwargs.get("litellm_params") or MappingProxyType({}), _SupportsGet, "get"
).get("metadata") or MappingProxyType({})
return str(
payload.get("session_id")
or require_attributes(request_metadata, _SupportsGet, "get").get("session_id")
or payload.get("trace_id")
or ""
)
def _is_trace_ingest(payload: StandardLoggingPayload) -> bool: def _is_trace_ingest(payload: StandardLoggingPayload) -> bool:
@ -128,7 +144,7 @@ def _is_trace_ingest(payload: StandardLoggingPayload) -> bool:
def spend_log_row_from_payload( def spend_log_row_from_payload(
payload: StandardLoggingPayload | SpendLogPayload, kwargs: Mapping[str, Any] payload: StandardLoggingPayload | SpendLogPayload, kwargs: Mapping[str, object]
) -> SpendLogRecord: ) -> SpendLogRecord:
metadata: Mapping[str, Any] = payload.get("metadata") or MappingProxyType({}) metadata: Mapping[str, Any] = payload.get("metadata") or MappingProxyType({})
hidden_params: Mapping[str, Any] = payload.get("hidden_params") or MappingProxyType({}) hidden_params: Mapping[str, Any] = payload.get("hidden_params") or MappingProxyType({})

View file

@ -7,7 +7,7 @@ from collections.abc import Iterable, Mapping, Sequence
from datetime import datetime from datetime import datetime
from itertools import chain from itertools import chain
from types import MappingProxyType from types import MappingProxyType
from typing import TYPE_CHECKING, Any, ClassVar, Final, Literal, Optional, cast, get_args from typing import TYPE_CHECKING, Any, ClassVar, Final, Literal, Optional, Protocol, cast, get_args
import httpx import httpx
from pydantic import TypeAdapter from pydantic import TypeAdapter
@ -20,6 +20,7 @@ from litellm.litellm_core_utils.core_helpers import (
get_metadata_variable_name_from_kwargs, get_metadata_variable_name_from_kwargs,
get_or_create_metadata_bucket, get_or_create_metadata_bucket,
redact_nested_match_and_regex_keys, redact_nested_match_and_regex_keys,
require_attributes,
) )
from litellm.secret_managers.main import str_to_bool from litellm.secret_managers.main import str_to_bool
from litellm.types.guardrails import ( from litellm.types.guardrails import (
@ -167,18 +168,22 @@ def _strict_guardrail_modes_enabled() -> bool:
return True if parsed is None else parsed return True if parsed is None else parsed
def get_session_id_from_request_data(request_data: dict[str, Any]) -> str | None: class _SupportsGet(Protocol):
def get(self, key: str, /) -> object: ...
def get_session_id_from_request_data(request_data: Mapping[str, object]) -> str | None:
"""Extract session_id from request data (litellm_session_id or metadata).""" """Extract session_id from request data (litellm_session_id or metadata)."""
session_id = request_data.get("litellm_session_id") session_id = request_data.get("litellm_session_id")
if session_id: if session_id:
return str(session_id) return str(session_id)
metadata: Final = request_data.get("metadata") or {} metadata: Final = require_attributes(request_data.get("metadata") or {}, _SupportsGet, "get")
session_id = metadata.get("session_id") session_id = metadata.get("session_id")
if session_id: if session_id:
return str(session_id) return str(session_id)
litellm_metadata: Final = request_data.get("litellm_metadata") or {} litellm_metadata: Final = require_attributes(request_data.get("litellm_metadata") or {}, _SupportsGet, "get")
session_id = litellm_metadata.get("session_id") session_id = litellm_metadata.get("session_id")
if session_id: if session_id:
return str(session_id) return str(session_id)

View file

@ -3,17 +3,29 @@ Functions for sending Email Alerts
""" """
import os import os
from typing import Final from collections.abc import Awaitable, Mapping, Sequence
from typing import TYPE_CHECKING, Final
from litellm._logging import verbose_logger, verbose_proxy_logger from litellm._logging import verbose_logger, verbose_proxy_logger
from litellm.repositories.team_repository import TeamRepository from litellm.repositories.team_repository import TeamRepository
from litellm.types.integrations.slack_alerting import WebhookEvent from litellm.types.integrations.slack_alerting import WebhookEvent
if TYPE_CHECKING:
from litellm.proxy.utils import PrismaClient
# we use this for the email header, please send a test email if you change this. verify it looks good on email # we use this for the email header, please send a test email if you change this. verify it looks good on email
LITELLM_LOGO_URL: Final = "https://litellm-listing.s3.amazonaws.com/litellm_logo.png" LITELLM_LOGO_URL: Final = "https://litellm-listing.s3.amazonaws.com/litellm_logo.png"
LITELLM_SUPPORT_CONTACT: Final = "support@berri.ai" LITELLM_SUPPORT_CONTACT: Final = "support@berri.ai"
def _query_raw_rows(
client: "PrismaClient", query: str, *args: object
) -> Awaitable[Sequence[Mapping[str, object]] | None]:
from litellm.proxy.utils import query_raw_rows
return query_raw_rows(client.db, query, *args)
async def get_all_team_member_emails(team_id: str | None = None) -> list: async def get_all_team_member_emails(team_id: str | None = None) -> list:
verbose_logger.debug("Email Alerting: Getting all team members for team_id=%s", team_id) verbose_logger.debug("Email Alerting: Getting all team members for team_id=%s", team_id)
if team_id is None: if team_id is None:
@ -51,17 +63,18 @@ async def get_all_team_member_emails(team_id: str | None = None) -> list:
WHERE user_id = ANY($1::TEXT[]); WHERE user_id = ANY($1::TEXT[]);
""" """
_result: Final = await prisma_client.db.query_raw(sql_query, _team_member_user_ids) _result: Final = await _query_raw_rows(prisma_client, sql_query, _team_member_user_ids)
verbose_logger.debug("Email Alerting: Got all Emails for team, emails=%s", _result) verbose_logger.debug("Email Alerting: Got all Emails for team, emails=%s", _result)
if _result is None: if _result is None:
return [] return []
emails: Final = [] emails: Final = [
for user in _result: user.get("user_email")
if user and isinstance(user, dict) and user.get("user_email", None) is not None: for user in _result
emails.append(user.get("user_email")) if user and isinstance(user, dict) and user.get("user_email", None) is not None
]
return emails return emails

View file

@ -123,7 +123,9 @@ class FocusLiteLLMDatabase:
ORDER BY ordinal_position; ORDER BY ordinal_position;
""" """
try: try:
columns_response: Final = await client.db.query_raw(info_query) from litellm.proxy.utils import query_raw_rows
columns_response: Final = await query_raw_rows(client.db, info_query)
return {"columns": columns_response, "table_name": "LiteLLM_DailyUserSpend"} return {"columns": columns_response, "table_name": "LiteLLM_DailyUserSpend"}
except Exception as exc: except Exception as exc:
raise RuntimeError(f"Error getting table info: {exc}") from exc raise RuntimeError(f"Error getting table info: {exc}") from exc

View file

@ -15,6 +15,7 @@ from typing_extensions import ReadOnly, TypedDict
import litellm import litellm
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
from litellm.integrations.custom_logger import CustomLogger from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.core_helpers import require_attributes, subscript
from litellm.litellm_core_utils.prompt_templates.common_utils import ( from litellm.litellm_core_utils.prompt_templates.common_utils import (
convert_content_list_to_str, convert_content_list_to_str,
get_content_from_model_response, get_content_from_model_response,
@ -60,6 +61,10 @@ class _JsonResponse(Protocol):
def json(self) -> object: ... def json(self) -> object: ...
class _SupportsGet(Protocol):
def get(self, key: str, /) -> object: ...
def _login_access_token(response: _GalileoLoginResponse) -> str: def _login_access_token(response: _GalileoLoginResponse) -> str:
"""Read the bearer token out of a Galileo login response body.""" """Read the bearer token out of a Galileo login response body."""
return response.json()["access_token"] return response.json()["access_token"]
@ -463,13 +468,13 @@ class GalileoObserve(CustomLogger):
pass pass
@staticmethod @staticmethod
def _build_prompt(kwargs: Mapping[str, Any]) -> dict[str, object]: def _build_prompt(kwargs: Mapping[str, object]) -> dict[str, object]:
optional_params: Final[Mapping[str, object]] = kwargs.get("optional_params", {}) or {} optional_params: Final = require_attributes(kwargs.get("optional_params", {}) or {}, _SupportsGet, "get")
prompt: Final[dict[str, object]] = {"messages": kwargs.get("messages")} prompt: Final[dict[str, object]] = {"messages": kwargs.get("messages")}
if optional_params.get("functions") is not None: if optional_params.get("functions") is not None:
prompt["functions"] = optional_params["functions"] prompt["functions"] = subscript(optional_params, "functions")
if optional_params.get("tools") is not None: if optional_params.get("tools") is not None:
prompt["tools"] = optional_params["tools"] prompt["tools"] = subscript(optional_params, "tools")
return prompt return prompt
@staticmethod @staticmethod

View file

@ -5,7 +5,7 @@ import os
import time import time
from collections.abc import Mapping from collections.abc import Mapping
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING, Any, Final from typing import TYPE_CHECKING, Any, Final, Protocol
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
from litellm._uuid import uuid from litellm._uuid import uuid
@ -16,7 +16,7 @@ from litellm.integrations.gcs_bucket.gcs_bucket_base import GCSBucketBase
from litellm.litellm_core_utils.cloud_storage_security import ( from litellm.litellm_core_utils.cloud_storage_security import (
sanitize_cloud_object_component, sanitize_cloud_object_component,
) )
from litellm.litellm_core_utils.core_helpers import as_str_mapping from litellm.litellm_core_utils.core_helpers import as_str_mapping, require_attributes
from litellm.litellm_core_utils.internal_call_metadata import logged_event_key from litellm.litellm_core_utils.internal_call_metadata import logged_event_key
from litellm.types.integrations.base_health_check import IntegrationHealthCheckStatus from litellm.types.integrations.base_health_check import IntegrationHealthCheckStatus
from litellm.types.integrations.gcs_bucket import * from litellm.types.integrations.gcs_bucket import *
@ -28,6 +28,10 @@ else:
VertexBase = Any VertexBase = Any
class _SupportsGetWithDefault(Protocol):
def get(self, key: str, default: str, /) -> object: ...
class GCSBucketLogger(GCSBucketBase, AdditionalLoggingUtils): class GCSBucketLogger(GCSBucketBase, AdditionalLoggingUtils):
def __init__(self, bucket_name: str | None = None) -> None: def __init__(self, bucket_name: str | None = None) -> None:
from litellm.proxy.proxy_server import premium_user from litellm.proxy.proxy_server import premium_user
@ -298,7 +302,7 @@ class GCSBucketLogger(GCSBucketBase, AdditionalLoggingUtils):
await self._send_individual_logs(items_to_process) await self._send_individual_logs(items_to_process)
def _get_object_name( def _get_object_name(
self, kwargs: Mapping[str, object], logging_payload: StandardLoggingPayload, response_obj: Any self, kwargs: Mapping[str, object], logging_payload: StandardLoggingPayload, response_obj: object
) -> str: ) -> str:
""" """
Get the object name to use for the current payload Get the object name to use for the current payload
@ -313,7 +317,7 @@ class GCSBucketLogger(GCSBucketBase, AdditionalLoggingUtils):
return f"{current_date}/custom-{uuid.uuid4().hex}-{safe_log_id}" return f"{current_date}/custom-{uuid.uuid4().hex}-{safe_log_id}"
if logging_payload.get("error_str", None) is not None: if logging_payload.get("error_str", None) is not None:
return self._generate_failure_object_name(request_date_str=current_date) return self._generate_failure_object_name(request_date_str=current_date)
raw_response_id: Final[object] = response_obj.get("id", "") raw_response_id: Final[object] = require_attributes(response_obj, _SupportsGetWithDefault, "get").get("id", "")
response_id: Final = raw_response_id if isinstance(raw_response_id, str) else "" response_id: Final = raw_response_id if isinstance(raw_response_id, str) else ""
return self._generate_success_object_name( return self._generate_success_object_name(
request_date_str=current_date, request_date_str=current_date,
@ -360,7 +364,7 @@ class GCSBucketLogger(GCSBucketBase, AdditionalLoggingUtils):
def _generate_success_object_name( def _generate_success_object_name(
self, self,
request_date_str: str, request_date_str: str,
response_id: str, response_id: object,
) -> str: ) -> str:
return f"{request_date_str}/{response_id}" return f"{request_date_str}/{response_id}"

View file

@ -132,7 +132,7 @@ class GenericPromptManager(CustomPromptManagement):
async def async_fetch_prompt_from_api( async def async_fetch_prompt_from_api(
self, prompt_id: str | None, prompt_spec: PromptSpec | None self, prompt_id: str | None, prompt_spec: PromptSpec | None
) -> dict[str, Any]: ) -> dict[str, object]:
""" """
Fetch a prompt from the API asynchronously. Fetch a prompt from the API asynchronously.
""" """

View file

@ -7,10 +7,12 @@ https://humanloop.com/
from typing import Final, cast from typing import Final, cast
import httpx import httpx
from pydantic import TypeAdapter
from typing_extensions import TypedDict from typing_extensions import TypedDict
import litellm import litellm
from litellm.caching import DualCache from litellm.caching import DualCache
from litellm.litellm_core_utils.core_helpers import as_object_list, as_str_mapping, decoded_json_object, subscript
from litellm.llms.custom_httpx.http_handler import get_httpx_client from litellm.llms.custom_httpx.http_handler import get_httpx_client
from litellm.secret_managers.main import get_secret_str from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import AllMessageValues from litellm.types.llms.openai import AllMessageValues
@ -19,6 +21,8 @@ from litellm.types.utils import StandardCallbackDynamicParams
from .custom_logger import CustomLogger from .custom_logger import CustomLogger
_DECODED_JSON: Final = TypeAdapter(object)
class PromptManagementClient(TypedDict): class PromptManagementClient(TypedDict):
prompt_id: str prompt_id: str
@ -79,18 +83,20 @@ class HumanLoopPromptManager(DualCache):
raise Exception(f"Error getting prompt from Humanloop: {e.response.text}") raise Exception(f"Error getting prompt from Humanloop: {e.response.text}")
json_response: Final = response.json() json_response: Final = response.json()
template_message: Final = json_response["template"] decoded_response: Final = _DECODED_JSON.validate_python(json_response)
if isinstance(template_message, dict): template_message: Final = subscript(decoded_response, "template")
template_messages = [template_message] template_mapping: Final = as_str_mapping(template_message)
elif isinstance(template_message, list): template_list: Final = as_object_list(template_message)
template_messages = template_message if template_mapping is not None:
template_messages = [template_mapping]
elif template_list is not None:
template_messages = template_list
else: else:
raise ValueError(f"Invalid template message type: {type(template_message)}") raise ValueError(f"Invalid template message type: {type(template_message)}")
template_model: Final = json_response["model"] template_model: Final = json_response["model"]
optional_params: Final = {} optional_params: Final = {
for k, v in json_response.items(): k: v for k, v in decoded_json_object(decoded_response).items() if k in litellm.OPENAI_CHAT_COMPLETION_PARAMS
if k in litellm.OPENAI_CHAT_COMPLETION_PARAMS: }
optional_params[k] = v
return PromptManagementClient( return PromptManagementClient(
prompt_id=humanloop_prompt_id, prompt_id=humanloop_prompt_id,
prompt_template=cast(list[AllMessageValues], template_messages), prompt_template=cast(list[AllMessageValues], template_messages),

View file

@ -3,7 +3,7 @@
import json import json
import os import os
from typing import Final, Literal from typing import Final, Literal, Protocol
import httpx import httpx
@ -11,6 +11,7 @@ import litellm
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
from litellm._uuid import uuid from litellm._uuid import uuid
from litellm.integrations.custom_logger import CustomLogger from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.core_helpers import require_attributes
from litellm.llms.custom_httpx.http_handler import ( from litellm.llms.custom_httpx.http_handler import (
HTTPHandler, HTTPHandler,
get_async_httpx_client, get_async_httpx_client,
@ -18,6 +19,10 @@ from litellm.llms.custom_httpx.http_handler import (
) )
class _SupportsGet(Protocol):
def get(self, key: str, default: object = None, /) -> object: ...
def get_utc_datetime(): def get_utc_datetime():
import datetime as dt import datetime as dt
from datetime import datetime from datetime import datetime
@ -70,10 +75,11 @@ class LagoLogger(CustomLogger):
if ( if (
isinstance(response_obj, litellm.ModelResponse) or isinstance(response_obj, litellm.EmbeddingResponse) isinstance(response_obj, litellm.ModelResponse) or isinstance(response_obj, litellm.EmbeddingResponse)
) and hasattr(response_obj, "usage"): ) and hasattr(response_obj, "usage"):
response_usage: Final = require_attributes(response_obj["usage"], _SupportsGet, "get")
usage = { usage = {
"prompt_tokens": response_obj["usage"].get("prompt_tokens", 0), "prompt_tokens": response_usage.get("prompt_tokens", 0),
"completion_tokens": response_obj["usage"].get("completion_tokens", 0), "completion_tokens": response_usage.get("completion_tokens", 0),
"total_tokens": response_obj["usage"].get("total_tokens"), "total_tokens": response_usage.get("total_tokens"),
} }
litellm_params: Final = kwargs.get("litellm_params", {}) or {} litellm_params: Final = kwargs.get("litellm_params", {}) or {}

View file

@ -7,6 +7,7 @@ from itertools import chain
from typing import TYPE_CHECKING, Any, Final, Optional, Protocol, runtime_checkable from typing import TYPE_CHECKING, Any, Final, Optional, Protocol, runtime_checkable
from opentelemetry import trace from opentelemetry import trace
from pydantic import TypeAdapter
import litellm import litellm
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
@ -33,6 +34,8 @@ from litellm.types.integrations.langfuse_otel import (
) )
from litellm.types.utils import StandardCallbackDynamicParams from litellm.types.utils import StandardCallbackDynamicParams
_DECODED_JSON: Final = TypeAdapter(object)
if TYPE_CHECKING: if TYPE_CHECKING:
from opentelemetry.trace import Span as _Span from opentelemetry.trace import Span as _Span
@ -643,7 +646,7 @@ def _tool_call_arguments(arguments: object) -> object:
if not isinstance(arguments, str): if not isinstance(arguments, str):
return arguments return arguments
try: try:
return json.loads(arguments) return _DECODED_JSON.validate_python(json.loads(arguments))
except json.JSONDecodeError: except json.JSONDecodeError:
return {} return {}

View file

@ -7,7 +7,7 @@ Relevant Issue: https://github.com/BerriAI/litellm/issues/13764
import json import json
from collections.abc import Mapping from collections.abc import Mapping
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING, Any, Final from typing import TYPE_CHECKING, Final, Protocol
from pydantic import BaseModel from pydantic import BaseModel
from typing_extensions import override from typing_extensions import override
@ -16,6 +16,7 @@ from litellm.integrations.opentelemetry_utils.base_otel_llm_obs_attributes impor
BaseLLMObsOTELAttributes, BaseLLMObsOTELAttributes,
safe_set_attribute, safe_set_attribute,
) )
from litellm.litellm_core_utils.core_helpers import require_attributes
from litellm.types.llms.openai import HttpxBinaryResponseContent, ResponsesAPIResponse from litellm.types.llms.openai import HttpxBinaryResponseContent, ResponsesAPIResponse
from litellm.types.utils import ( from litellm.types.utils import (
EmbeddingResponse, EmbeddingResponse,
@ -30,6 +31,10 @@ if TYPE_CHECKING:
from opentelemetry.trace import Span from opentelemetry.trace import Span
class _SupportsGet(Protocol):
def get(self, key: str, /) -> object: ...
def serialize_langfuse_datetime(value: datetime) -> str: def serialize_langfuse_datetime(value: datetime) -> str:
return json.dumps(value.astimezone().isoformat()) return json.dumps(value.astimezone().isoformat())
@ -87,11 +92,11 @@ def get_output_content_by_type(
class LangfuseLLMObsOTELAttributes(BaseLLMObsOTELAttributes): class LangfuseLLMObsOTELAttributes(BaseLLMObsOTELAttributes):
@staticmethod @staticmethod
@override @override
def set_messages(span: "Span", kwargs: dict[str, Any]): def set_messages(span: "Span", kwargs: Mapping[str, object]):
prompt: Final = {"messages": kwargs.get("messages")} prompt: Final = {"messages": kwargs.get("messages")}
optional_params: Final = kwargs.get("optional_params", {}) optional_params: Final = kwargs.get("optional_params", {})
functions: Final = optional_params.get("functions") functions: Final = require_attributes(optional_params, _SupportsGet, "get").get("functions")
tools: Final = optional_params.get("tools") tools: Final = require_attributes(optional_params, _SupportsGet, "get").get("tools")
if functions is not None: if functions is not None:
prompt["functions"] = functions prompt["functions"] = functions
if tools is not None: if tools is not None:

View file

@ -1,9 +1,11 @@
import json import json
import threading import threading
from typing import TYPE_CHECKING, Any, Final from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, Final, Protocol
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
from litellm.integrations.custom_logger import CustomLogger from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.core_helpers import attribute_or_none, iter_items, require_attributes
if TYPE_CHECKING: if TYPE_CHECKING:
from litellm.types.utils import StandardLoggingPayload from litellm.types.utils import StandardLoggingPayload
@ -11,6 +13,25 @@ else:
StandardLoggingPayload = Any StandardLoggingPayload = Any
class _ChoiceWithMessage(Protocol):
@property
def message(self) -> object: ...
class _ExcludeNoneDumpable(Protocol):
def model_dump(self, *, exclude_none: bool) -> object: ...
def _dumped_choice_message(choice: object) -> object:
message: Final = require_attributes(choice, _ChoiceWithMessage, "message").message
return require_attributes(message, _ExcludeNoneDumpable, "model_dump").model_dump(exclude_none=True)
def _output_messages(response_obj: object) -> Sequence[object]:
choices: Final = attribute_or_none(response_obj, "choices") if hasattr(response_obj, "choices") else ()
return [_dumped_choice_message(c) for c in iter_items(choices)]
class MlflowLogger(CustomLogger): class MlflowLogger(CustomLogger):
def __init__(self): def __init__(self):
from mlflow.tracking import MlflowClient from mlflow.tracking import MlflowClient
@ -62,7 +83,7 @@ class MlflowLogger(CustomLogger):
inputs: Final = self._construct_input(kwargs) inputs: Final = self._construct_input(kwargs)
input_messages: Final = inputs.get("messages", []) input_messages: Final = inputs.get("messages", [])
output_messages: Final = [c.message.model_dump(exclude_none=True) for c in getattr(response_obj, "choices", [])] output_messages: Final = _output_messages(response_obj)
if messages := [*input_messages, *output_messages]: if messages := [*input_messages, *output_messages]:
set_span_chat_messages(span, messages) set_span_chat_messages(span, messages)
if tools := inputs.get("tools"): if tools := inputs.get("tools"):

View file

@ -4,13 +4,13 @@
import json import json
import os import os
from collections.abc import Mapping from collections.abc import Mapping
from typing import Final from typing import Final, Protocol
import httpx import httpx
import litellm import litellm
from litellm.integrations.custom_logger import CustomLogger from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.core_helpers import as_str_mapping from litellm.litellm_core_utils.core_helpers import as_str_mapping, require_attributes
from litellm.litellm_core_utils.internal_call_metadata import logged_event_key from litellm.litellm_core_utils.internal_call_metadata import logged_event_key
from litellm.llms.custom_httpx.http_handler import ( from litellm.llms.custom_httpx.http_handler import (
HTTPHandler, HTTPHandler,
@ -19,6 +19,10 @@ from litellm.llms.custom_httpx.http_handler import (
) )
class _SupportsGet(Protocol):
def get(self, key: str, default: object = None, /) -> object: ...
def get_utc_datetime(): def get_utc_datetime():
import datetime as dt import datetime as dt
from datetime import datetime from datetime import datetime
@ -60,10 +64,11 @@ class OpenMeterLogger(CustomLogger):
if ( if (
isinstance(response_obj, litellm.ModelResponse) or isinstance(response_obj, litellm.EmbeddingResponse) isinstance(response_obj, litellm.ModelResponse) or isinstance(response_obj, litellm.EmbeddingResponse)
) and hasattr(response_obj, "usage"): ) and hasattr(response_obj, "usage"):
response_usage: Final = require_attributes(response_obj["usage"], _SupportsGet, "get")
usage = { usage = {
"prompt_tokens": response_obj["usage"].get("prompt_tokens", 0), "prompt_tokens": response_usage.get("prompt_tokens", 0),
"completion_tokens": response_obj["usage"].get("completion_tokens", 0), "completion_tokens": response_usage.get("completion_tokens", 0),
"total_tokens": response_obj["usage"].get("total_tokens"), "total_tokens": response_usage.get("total_tokens"),
} }
# OPENMETER_TRUST_REQUEST_USER (default "true"): when set to "false", # OPENMETER_TRUST_REQUEST_USER (default "true"): when set to "false",

View file

@ -6,7 +6,7 @@ from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime
from types import MappingProxyType from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Final, cast from typing import TYPE_CHECKING, Any, Final, Protocol, cast
from pydantic import ConfigDict, TypeAdapter from pydantic import ConfigDict, TypeAdapter
from typing_extensions import ReadOnly, TypedDict from typing_extensions import ReadOnly, TypedDict
@ -35,6 +35,7 @@ from litellm.integrations.otel.model.metadata import flatten_metadata
from litellm.integrations.otel.model.semconv import LiteLLM, Metric from litellm.integrations.otel.model.semconv import LiteLLM, Metric
from litellm.integrations.otel.plumbing.otlp_tls import resolve_otlp_http_tls from litellm.integrations.otel.plumbing.otlp_tls import resolve_otlp_http_tls
from litellm.integrations.otel.routing import routing_decision_attributes from litellm.integrations.otel.routing import routing_decision_attributes
from litellm.litellm_core_utils.core_helpers import iter_items, require_attributes
from litellm.litellm_core_utils.internal_call_metadata import is_unbilled_non_inference_call_from_params from litellm.litellm_core_utils.internal_call_metadata import is_unbilled_non_inference_call_from_params
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
from litellm.litellm_core_utils.secret_redaction import redact_string from litellm.litellm_core_utils.secret_redaction import redact_string
@ -47,7 +48,6 @@ from litellm.secret_managers.main import get_secret_bool, str_to_bool
from litellm.types.integrations.otel_span_attributes import SpanAttributes from litellm.types.integrations.otel_span_attributes import SpanAttributes
from litellm.types.services import ServiceLoggerPayload from litellm.types.services import ServiceLoggerPayload
from litellm.types.utils import ( from litellm.types.utils import (
ChatCompletionMessageToolCall,
CostBreakdown, CostBreakdown,
Function, Function,
LLMResponseTypes, LLMResponseTypes,
@ -130,6 +130,22 @@ def _text_as_stored_on_span(span: Span, key: str, text: str) -> str:
_MAX_DYNAMIC_TRACER_PROVIDERS: Final = 256 _MAX_DYNAMIC_TRACER_PROVIDERS: Final = 256
class _SupportsGet(Protocol):
def get(self, key: str, /) -> object: ...
def _response_field(response_obj: object, key: str) -> object:
if not response_obj:
return None
return require_attributes(response_obj, _SupportsGet, "get").get(key)
def _billed_usage(response_obj: object, call_type: str | None, litellm_params: Mapping[str, object] | None) -> object:
if not response_obj or is_unbilled_non_inference_call_from_params(call_type, litellm_params, response_obj):
return None
return _response_field(response_obj, "usage")
class _CacheHitView(TypedDict, total=False): class _CacheHitView(TypedDict, total=False):
cache_hit: ReadOnly[object] cache_hit: ReadOnly[object]
saved_cache_cost: ReadOnly[object] saved_cache_cost: ReadOnly[object]
@ -2407,17 +2423,17 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
@staticmethod @staticmethod
def _tool_calls_kv_pair( def _tool_calls_kv_pair(
tool_calls: list[ChatCompletionMessageToolCall], tool_calls: Iterable[object],
) -> dict[str, object]: ) -> dict[str, object]:
kv_pairs: Final[dict[str, object]] = {} kv_pairs: Final[dict[str, object]] = {}
for idx, tool_call in enumerate(tool_calls): for idx, tool_call in enumerate(tool_calls):
_function = tool_call.get("function") _function: object = require_attributes(tool_call, _SupportsGet, "get").get("function")
if not _function: if not _function:
continue continue
keys = Function.__annotations__.keys() keys = Function.__annotations__.keys()
for key in keys: for key in keys:
_value = _function.get(key) _value = require_attributes(_function, _SupportsGet, "get").get(key)
if _value: if _value:
kv_pairs[f"{SpanAttributes.LLM_COMPLETIONS.value}.{idx}.function_call.{key}"] = _value kv_pairs[f"{SpanAttributes.LLM_COMPLETIONS.value}.{idx}.function_call.{key}"] = _value
@ -2584,7 +2600,7 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
# but Embeddings and Image-gen responses do not. Fall back to # but Embeddings and Image-gen responses do not. Fall back to
# the litellm call ID so every call type can be correlated # the litellm call ID so every call type can be correlated
# across LiteLLM UI, Phoenix traces, and provider logs (Issue #8). # across LiteLLM UI, Phoenix traces, and provider logs (Issue #8).
response_id = (response_obj.get("id") if response_obj else None) or standard_logging_payload.get("id") response_id = _response_field(response_obj, "id") or standard_logging_payload.get("id")
if response_id: if response_id:
self.safe_set_attribute( self.safe_set_attribute(
span=span, span=span,
@ -2601,42 +2617,37 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
) )
# The model used to generate the response. # The model used to generate the response.
if response_obj and response_obj.get("model"): response_model: Final = _response_field(response_obj, "model")
if response_model:
self.safe_set_attribute( self.safe_set_attribute(
span=span, span=span,
key=SpanAttributes.LLM_RESPONSE_MODEL.value, key=SpanAttributes.LLM_RESPONSE_MODEL.value,
value=response_obj.get("model"), value=response_model,
) )
self._set_service_tier_attributes(span=span, standard_logging_payload=standard_logging_payload) self._set_service_tier_attributes(span=span, standard_logging_payload=standard_logging_payload)
usage: Final = ( usage: Final = _billed_usage(response_obj, kwargs.get("call_type"), litellm_params)
response_obj.get("usage")
if response_obj
and not is_unbilled_non_inference_call_from_params(
kwargs.get("call_type"), litellm_params, response_obj
)
else None
)
if usage: if usage:
usage_fields: Final = require_attributes(usage, _SupportsGet, "get")
self.safe_set_attribute( self.safe_set_attribute(
span=span, span=span,
key=SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS.value, key=SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS.value,
value=usage.get("total_tokens"), value=usage_fields.get("total_tokens"),
) )
# The number of tokens used in the LLM response (completion). # The number of tokens used in the LLM response (completion).
self.safe_set_attribute( self.safe_set_attribute(
span=span, span=span,
key=SpanAttributes.GEN_AI_USAGE_OUTPUT_TOKENS.value, key=SpanAttributes.GEN_AI_USAGE_OUTPUT_TOKENS.value,
value=usage.get("completion_tokens"), value=usage_fields.get("completion_tokens"),
) )
# The number of tokens used in the LLM prompt. # The number of tokens used in the LLM prompt.
self.safe_set_attribute( self.safe_set_attribute(
span=span, span=span,
key=SpanAttributes.GEN_AI_USAGE_INPUT_TOKENS.value, key=SpanAttributes.GEN_AI_USAGE_INPUT_TOKENS.value,
value=usage.get("prompt_tokens"), value=usage_fields.get("prompt_tokens"),
) )
######################################################################## ########################################################################
@ -2712,7 +2723,8 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
########## LLM Response Attributes ########## ########## LLM Response Attributes ##########
############################################# #############################################
if response_obj is not None: if response_obj is not None:
if response_obj.get("choices"): response_view: Final = require_attributes(response_obj, _SupportsGet, "get")
if response_view.get("choices"):
transformed_choices: Final = self._transform_choices_to_otel_semantic_conventions( transformed_choices: Final = self._transform_choices_to_otel_semantic_conventions(
response_obj.get("choices") response_obj.get("choices")
) )
@ -2722,10 +2734,13 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
value=safe_dumps(transformed_choices), value=safe_dumps(transformed_choices),
) )
finish_reasons: Final = [] choice_views: Final = [
for idx, choice in enumerate(response_obj.get("choices")): require_attributes(choice, _SupportsGet, "get")
if choice.get("finish_reason"): for choice in iter_items(response_view.get("choices"))
finish_reasons.append(choice.get("finish_reason")) ]
finish_reasons: Final = [
choice.get("finish_reason") for choice in choice_views if choice.get("finish_reason")
]
if finish_reasons: if finish_reasons:
self.safe_set_attribute( self.safe_set_attribute(
@ -2734,11 +2749,11 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
value=safe_dumps(finish_reasons), value=safe_dumps(finish_reasons),
) )
for idx, choice in enumerate(response_obj.get("choices")): for choice in choice_views:
if choice.get("finish_reason"): if choice.get("finish_reason"):
message = choice.get("message") tool_calls = tool_calls_with_server_tool_calls(choice.get("message"))
if tool_calls := tool_calls_with_server_tool_calls(message): if tool_calls:
kv_pairs = OpenTelemetry._tool_calls_kv_pair(list(tool_calls)) kv_pairs = OpenTelemetry._tool_calls_kv_pair(tool_calls)
for key, value in kv_pairs.items(): for key, value in kv_pairs.items():
self.safe_set_attribute( self.safe_set_attribute(
span=span, span=span,
@ -2746,13 +2761,13 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
value=value, value=value,
) )
elif response_obj.get("output"): elif response_view.get("output"):
# Responses API: ResponsesAPIResponse has an "output" # Responses API: ResponsesAPIResponse has an "output"
# list instead of "choices". Each item with # list instead of "choices". Each item with
# type="message" contains a "content" list of # type="message" contains a "content" list of
# OutputText objects (type="output_text"). # OutputText objects (type="output_text").
output_items: Final = response_obj.get("output") output_items: Final = response_view.get("output")
output_messages: Final = self._transform_responses_api_output_to_otel(output_items) output_messages: Final = self._transform_responses_api_output_to_otel(iter_items(output_items))
if output_messages: if output_messages:
self.safe_set_attribute( self.safe_set_attribute(
span=span, span=span,
@ -2766,7 +2781,7 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
# ChatCompletionMessageToolCall format expected by # ChatCompletionMessageToolCall format expected by
# _tool_calls_kv_pair. # _tool_calls_kv_pair.
tool_calls = [] tool_calls = []
for out_item in output_items: for out_item in iter_items(output_items):
item_d = self._to_dict(out_item) item_d = self._to_dict(out_item)
if item_d and item_d.get("type") == "function_call": if item_d and item_d.get("type") == "function_call":
tool_calls.append( tool_calls.append(
@ -2787,7 +2802,7 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
) )
# Extract finish reason from ResponsesAPIResponse.status # Extract finish reason from ResponsesAPIResponse.status
status: Final = response_obj.get("status") status: Final = response_view.get("status")
if status: if status:
self.safe_set_attribute( self.safe_set_attribute(
span=span, span=span,
@ -2907,7 +2922,7 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
return obj.model_dump() return obj.model_dump()
return None return None
def _transform_responses_api_output_to_otel(self, output: list) -> list[dict]: def _transform_responses_api_output_to_otel(self, output: Iterable[object]) -> list[dict]:
""" """
Transform Responses API output items into OTEL GenAI 1.38 format. Transform Responses API output items into OTEL GenAI 1.38 format.

View file

@ -1,4 +1,5 @@
from abc import ABC from abc import ABC
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, Final from typing import TYPE_CHECKING, Any, Final
if TYPE_CHECKING: if TYPE_CHECKING:
@ -7,7 +8,7 @@ if TYPE_CHECKING:
class BaseLLMObsOTELAttributes(ABC): class BaseLLMObsOTELAttributes(ABC):
@staticmethod @staticmethod
def set_messages(span: "Span", kwargs: dict[str, Any]): def set_messages(span: "Span", kwargs: Mapping[str, object]):
pass pass
@staticmethod @staticmethod

View file

@ -11,6 +11,7 @@ identical metrics. The attribute cardinality filter is reused from v1 by import
from collections.abc import Mapping from collections.abc import Mapping
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from types import MappingProxyType
from typing import Any, Final, Literal, Protocol, TypeAlias from typing import Any, Final, Literal, Protocol, TypeAlias
from opentelemetry.metrics import Histogram, Meter from opentelemetry.metrics import Histogram, Meter
@ -34,6 +35,7 @@ from litellm.integrations.otel.model.semconv import (
resolve_provider, resolve_provider,
) )
from litellm.integrations.otel.model.utils import to_seconds from litellm.integrations.otel.model.utils import to_seconds
from litellm.litellm_core_utils.core_helpers import attribute_or_none, require_attributes
from litellm.litellm_core_utils.internal_call_metadata import is_unbilled_non_inference_call_from_params from litellm.litellm_core_utils.internal_call_metadata import is_unbilled_non_inference_call_from_params
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
@ -177,7 +179,14 @@ class _MetricKwargs(TypedDict, total=False):
end_time: ReadOnly[datetime | float | str | None] end_time: ReadOnly[datetime | float | str | None]
def resolve_error_type(kwargs: Mapping[str, Any]) -> str: class _SupportsGet(Protocol):
def get(self, key: str, /) -> object: ...
_NO_FIELDS: Final[Mapping[str, object]] = MappingProxyType({})
def resolve_error_type(kwargs: Mapping[str, object]) -> str:
"""The ``error.type`` value for a failed request. """The ``error.type`` value for a failed request.
Bounded by construction: the mapped provider exception's class name (the same Bounded by construction: the mapped provider exception's class name (the same
@ -188,8 +197,13 @@ def resolve_error_type(kwargs: Mapping[str, Any]) -> str:
span and its exception event, where high cardinality is free. span and its exception event, where high cardinality is free.
""" """
std_log: Final = kwargs.get("standard_logging_object") std_log: Final = kwargs.get("standard_logging_object")
info: Final = getattr(std_log, "error_information", None) or (std_log or {}).get("error_information") or {} info: Final = (
error_class: Final = info.get("error_class") or info.get("error_code") attribute_or_none(std_log, "error_information")
or require_attributes(std_log or _NO_FIELDS, _SupportsGet, "get").get("error_information")
or _NO_FIELDS
)
info_fields: Final = require_attributes(info, _SupportsGet, "get")
error_class: Final = info_fields.get("error_class") or info_fields.get("error_code")
if error_class: if error_class:
return str(error_class) return str(error_class)
exception: Final = kwargs.get("exception") exception: Final = kwargs.get("exception")

View file

@ -4,9 +4,12 @@ import os
import traceback import traceback
from typing import Final from typing import Final
from pydantic import BaseModel from pydantic import BaseModel, TypeAdapter
import litellm import litellm
from litellm.litellm_core_utils.core_helpers import decoded_json_object
_DECODED_JSON: Final = TypeAdapter(object)
class PromptLayerLogger: class PromptLayerLogger:
@ -61,8 +64,8 @@ class PromptLayerLogger:
}, },
) )
response_json: Final = request_response.json() response_json: Final = decoded_json_object(_DECODED_JSON.validate_python(request_response.json()))
if not request_response.json().get("success", False): if not response_json.get("success", False):
raise Exception("Promptlayer did not successfully log the response!") raise Exception("Promptlayer did not successfully log the response!")
print_verbose(f"Prompt Layer Logging: success - final response object: {request_response.text}") print_verbose(f"Prompt Layer Logging: success - final response object: {request_response.text}")

View file

@ -4,7 +4,8 @@ import base64
import json import json
import os import os
from collections.abc import Mapping from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, Final from types import MappingProxyType
from typing import TYPE_CHECKING, Final, Protocol
from opentelemetry.trace import Status, StatusCode from opentelemetry.trace import Status, StatusCode
from typing_extensions import override from typing_extensions import override
@ -19,6 +20,7 @@ from litellm.integrations.opentelemetry_utils.base_otel_llm_obs_attributes impor
BaseLLMObsOTELAttributes, BaseLLMObsOTELAttributes,
safe_set_attribute, safe_set_attribute,
) )
from litellm.litellm_core_utils.core_helpers import has_attributes, require_attributes
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
from litellm.types.integrations.weave_otel import WeaveOtelConfig, WeaveSpanAttributes from litellm.types.integrations.weave_otel import WeaveOtelConfig, WeaveSpanAttributes
from litellm.types.utils import StandardCallbackDynamicParams from litellm.types.utils import StandardCallbackDynamicParams
@ -34,6 +36,21 @@ WEAVE_BASE_URL: Final = "https://trace.wandb.ai"
WEAVE_OTEL_ENDPOINT: Final = "/otel/v1/traces" WEAVE_OTEL_ENDPOINT: Final = "/otel/v1/traces"
_NO_FIELDS: Final[Mapping[str, object]] = MappingProxyType({})
class _SupportsGet(Protocol):
def get(self, key: str, /) -> object: ...
class _SupportsReplace(Protocol):
def replace(self, old: str, new: str, /) -> object: ...
class _ModelDumpable(Protocol):
def model_dump(self) -> object: ...
class WeaveLLMObsOTELAttributes(BaseLLMObsOTELAttributes): class WeaveLLMObsOTELAttributes(BaseLLMObsOTELAttributes):
""" """
Weave-specific LLM observability OTEL attributes. Weave-specific LLM observability OTEL attributes.
@ -44,15 +61,15 @@ class WeaveLLMObsOTELAttributes(BaseLLMObsOTELAttributes):
@staticmethod @staticmethod
@override @override
def set_messages(span: Span, kwargs: dict[str, Any]): def set_messages(span: Span, kwargs: Mapping[str, object]):
"""Set input messages as span attributes using OpenInference conventions.""" """Set input messages as span attributes using OpenInference conventions."""
messages: Final = kwargs.get("messages") or [] messages: Final = kwargs.get("messages") or []
optional_params: Final = kwargs.get("optional_params") or {} optional_params: Final = kwargs.get("optional_params") or _NO_FIELDS
prompt: Final = {"messages": messages} prompt: Final = {"messages": messages}
functions: Final = optional_params.get("functions") functions: Final = require_attributes(optional_params, _SupportsGet, "get").get("functions")
tools: Final = optional_params.get("tools") tools: Final = require_attributes(optional_params, _SupportsGet, "get").get("tools")
if functions is not None: if functions is not None:
prompt["functions"] = functions prompt["functions"] = functions
if tools is not None: if tools is not None:
@ -60,7 +77,7 @@ class WeaveLLMObsOTELAttributes(BaseLLMObsOTELAttributes):
safe_set_attribute(span, OpenInferenceSpanAttributes.INPUT_VALUE, json.dumps(prompt)) safe_set_attribute(span, OpenInferenceSpanAttributes.INPUT_VALUE, json.dumps(prompt))
def _set_weave_specific_attributes(span: Span, kwargs: Mapping[str, Any], response_obj: Any): def _set_weave_specific_attributes(span: Span, kwargs: Mapping[str, object], response_obj: object):
""" """
Sets Weave-specific metadata attributes onto the OTEL span. Sets Weave-specific metadata attributes onto the OTEL span.
@ -69,25 +86,27 @@ def _set_weave_specific_attributes(span: Span, kwargs: Mapping[str, Any], respon
""" """
# Extract all needed data upfront # Extract all needed data upfront
litellm_params: Final = kwargs.get("litellm_params") or {} litellm_params: Final = kwargs.get("litellm_params") or _NO_FIELDS
# optional_params = kwargs.get("optional_params") or {} # optional_params = kwargs.get("optional_params") or {}
metadata: Final = kwargs.get("metadata") or {} metadata: Final = kwargs.get("metadata") or _NO_FIELDS
model: Final = kwargs.get("model") or "" model: Final = kwargs.get("model") or ""
custom_llm_provider: Final = litellm_params.get("custom_llm_provider") or "" custom_llm_provider: Final = (
require_attributes(litellm_params, _SupportsGet, "get").get("custom_llm_provider") or ""
)
# Weave supports a custom display name and will default to the model name if not provided. # Weave supports a custom display name and will default to the model name if not provided.
display_name = metadata.get("display_name") display_name = require_attributes(metadata, _SupportsGet, "get").get("display_name")
if not display_name and model: if not display_name and model:
if custom_llm_provider: if custom_llm_provider:
display_name = f"{custom_llm_provider}/{model}" display_name = f"{custom_llm_provider}/{model}"
else: else:
display_name = model display_name = model
if display_name: if display_name:
display_name = display_name.replace("/", "__") display_name = require_attributes(display_name, _SupportsReplace, "replace").replace("/", "__")
safe_set_attribute(span, WeaveSpanAttributes.DISPLAY_NAME.value, display_name) safe_set_attribute(span, WeaveSpanAttributes.DISPLAY_NAME.value, display_name)
# Weave threads are OpenInference sessions. # Weave threads are OpenInference sessions.
if (session_id := metadata.get("session_id")) is not None: if (session_id := require_attributes(metadata, _SupportsGet, "get").get("session_id")) is not None:
if isinstance(session_id, (list, dict)): if isinstance(session_id, (list, dict)):
session_id = safe_dumps(session_id) session_id = safe_dumps(session_id)
safe_set_attribute(span, WeaveSpanAttributes.THREAD_ID.value, session_id) safe_set_attribute(span, WeaveSpanAttributes.THREAD_ID.value, session_id)
@ -97,7 +116,7 @@ def _set_weave_specific_attributes(span: Span, kwargs: Mapping[str, Any], respon
# but we override them here to better match Weave's expectations # but we override them here to better match Weave's expectations
if response_obj: if response_obj:
output_dict = None output_dict = None
if hasattr(response_obj, "model_dump"): if has_attributes(response_obj, _ModelDumpable, "model_dump"):
output_dict = response_obj.model_dump() output_dict = response_obj.model_dump()
elif hasattr(response_obj, "get"): elif hasattr(response_obj, "get"):
output_dict = response_obj output_dict = response_obj

View file

@ -7,9 +7,19 @@ to this format for consistent interception and execution.
""" """
from collections.abc import Mapping from collections.abc import Mapping
from typing import Any, Final from typing import Final, Protocol
from litellm.constants import LITELLM_WEB_SEARCH_TOOL_NAME from litellm.constants import LITELLM_WEB_SEARCH_TOOL_NAME
from litellm.litellm_core_utils.core_helpers import require_attributes
class _SupportsGetWithDefault(Protocol):
def get(self, key: str, default: str, /) -> object: ...
class _SupportsStartswith(Protocol):
def startswith(self, prefix: str, /) -> object: ...
_WEB_SEARCH_TOOL_DESCRIPTION: Final = ( _WEB_SEARCH_TOOL_DESCRIPTION: Final = (
"Search the web for information. Use this when you need current " "Search the web for information. Use this when you need current "
@ -164,7 +174,7 @@ def is_chat_completion_request_web_search_tool(tool: Mapping[str, object]) -> bo
return tool.get("type") == "web_search" or is_web_search_tool(dict(tool)) return tool.get("type") == "web_search" or is_web_search_tool(dict(tool))
def is_web_search_tool_chat_completion(tool: dict[str, Any]) -> bool: def is_web_search_tool_chat_completion(tool: Mapping[str, object]) -> bool:
""" """
Check if a tool is a web search tool for Chat Completions API (strict check). Check if a tool is a web search tool for Chat Completions API (strict check).
@ -197,7 +207,7 @@ def is_web_search_tool_chat_completion(tool: dict[str, Any]) -> bool:
# Check for OpenAI format: {"type": "function", "function": {"name": "litellm_web_search"}} # Check for OpenAI format: {"type": "function", "function": {"name": "litellm_web_search"}}
if tool_type == "function" and "function" in tool: if tool_type == "function" and "function" in tool:
function_def: Final = tool.get("function", {}) function_def: Final = tool.get("function", {})
function_name: Final = function_def.get("name", "") function_name: Final = require_attributes(function_def, _SupportsGetWithDefault, "get").get("name", "")
if function_name == LITELLM_WEB_SEARCH_TOOL_NAME: if function_name == LITELLM_WEB_SEARCH_TOOL_NAME:
return True return True
@ -229,7 +239,7 @@ def is_anthropic_native_web_search_tool(tool: Mapping[str, object]) -> bool:
return tool_type.startswith("web_search_") and tool_type != "function" return tool_type.startswith("web_search_") and tool_type != "function"
def is_web_search_tool(tool: dict[str, Any]) -> bool: def is_web_search_tool(tool: Mapping[str, object]) -> bool:
""" """
Check if a tool is a web search tool (native or LiteLLM standard). Check if a tool is a web search tool (native or LiteLLM standard).
@ -281,7 +291,7 @@ def is_web_search_tool(tool: dict[str, Any]) -> bool:
# Check for OpenAI format: {"type": "function", "function": {"name": "..."}} # Check for OpenAI format: {"type": "function", "function": {"name": "..."}}
if tool_type == "function" and "function" in tool: if tool_type == "function" and "function" in tool:
function_def: Final = tool.get("function", {}) function_def: Final = tool.get("function", {})
function_name: Final = function_def.get("name", "") function_name: Final = require_attributes(function_def, _SupportsGetWithDefault, "get").get("name", "")
if function_name == LITELLM_WEB_SEARCH_TOOL_NAME: if function_name == LITELLM_WEB_SEARCH_TOOL_NAME:
return True return True
@ -290,7 +300,7 @@ def is_web_search_tool(tool: dict[str, Any]) -> bool:
return True return True
# Check for native Anthropic web_search_* types # Check for native Anthropic web_search_* types
if tool_type.startswith("web_search_"): if require_attributes(tool_type, _SupportsStartswith, "startswith").startswith("web_search_"):
return True return True
# Check for Claude Code's web_search with a type field # Check for Claude Code's web_search with a type field

View file

@ -1,11 +1,13 @@
from typing import Final from typing import Final
from litellm.litellm_core_utils.core_helpers import iter_items, subscript
imported_openAIResponse = True imported_openAIResponse = True
try: try:
import io import io
import logging import logging
from collections.abc import Mapping from collections.abc import Mapping
from typing import Any, Literal, Protocol, TypeVar from typing import Literal, Protocol, TypeVar
from wandb.sdk.data_types import trace_tree from wandb.sdk.data_types import trace_tree
@ -25,7 +27,7 @@ try:
class OpenAIRequestResponseResolver: class OpenAIRequestResponseResolver:
def __call__( def __call__(
self, self,
request: dict[str, Any], request: Mapping[str, object],
response: OpenAIResponse, response: OpenAIResponse,
time_elapsed: float, time_elapsed: float,
) -> trace_tree.WBTraceTree | None: ) -> trace_tree.WBTraceTree | None:
@ -110,14 +112,14 @@ try:
def _resolve_chat_completion( def _resolve_chat_completion(
self, self,
request: dict[str, Any], request: Mapping[str, object],
response: OpenAIResponse, response: OpenAIResponse,
time_elapsed: float, time_elapsed: float,
) -> trace_tree.WBTraceTree: ) -> trace_tree.WBTraceTree:
"""Resolves the request and response objects for `openai.Completion`.""" """Resolves the request and response objects for `openai.Completion`."""
prompt: Final = io.StringIO() prompt: Final = io.StringIO()
for message in request["messages"]: for message in iter_items(request["messages"]):
prompt.write(f"\n\n**{message['role']}**: {message['content']}\n") prompt.write(f"\n\n**{subscript(message, 'role')}**: {subscript(message, 'content')}\n")
request_str: Final = prompt.getvalue() request_str: Final = prompt.getvalue()
choices: Final = [ choices: Final = [

View file

@ -8,11 +8,11 @@ This module handles transforming between:
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from types import MappingProxyType from types import MappingProxyType
from typing import Final, cast from typing import Final, Protocol, cast
from pydantic import BaseModel from pydantic import BaseModel
from litellm.litellm_core_utils.core_helpers import billed_usage_cost from litellm.litellm_core_utils.core_helpers import attribute_or_none, billed_usage_cost, has_attributes
from litellm.types.interactions import ( from litellm.types.interactions import (
InteractionInput, InteractionInput,
InteractionsAPIOptionalRequestParams, InteractionsAPIOptionalRequestParams,
@ -27,6 +27,10 @@ from litellm.types.llms.openai import (
_STEP_TYPE_ROLES: Final = MappingProxyType({"user_input": "user", "model_output": "assistant"}) _STEP_TYPE_ROLES: Final = MappingProxyType({"user_input": "user", "model_output": "assistant"})
class _IsoFormattable(Protocol):
def isoformat(self) -> object: ...
class LiteLLMResponsesInteractionsConfig: class LiteLLMResponsesInteractionsConfig:
"""Configuration class for transforming between Interactions API and Responses API.""" """Configuration class for transforming between Interactions API and Responses API."""
@ -232,12 +236,12 @@ class LiteLLMResponsesInteractionsConfig:
) )
# Convert created_at to ISO string # Convert created_at to ISO string
created_at: Final = getattr(responses_response, "created_at", None) created_at: Final = attribute_or_none(responses_response, "created_at")
if isinstance(created_at, int): if isinstance(created_at, int):
from datetime import datetime from datetime import datetime
created = datetime.fromtimestamp(created_at).isoformat() created = datetime.fromtimestamp(created_at).isoformat()
elif created_at is not None and hasattr(created_at, "isoformat"): elif created_at is not None and has_attributes(created_at, _IsoFormattable, "isoformat"):
created = created_at.isoformat() created = created_at.isoformat()
else: else:
created = None created = None

View file

@ -7,8 +7,9 @@ from the Google Interactions API, similar to the responses API streaming iterato
import asyncio import asyncio
import json import json
from collections.abc import Mapping
from datetime import datetime from datetime import datetime
from typing import Any, Final from typing import Final, Protocol
import httpx import httpx
from pydantic import ConfigDict, TypeAdapter from pydantic import ConfigDict, TypeAdapter
@ -16,7 +17,7 @@ from pydantic import ConfigDict, TypeAdapter
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
from litellm.constants import STREAM_SSE_DONE_STRING from litellm.constants import STREAM_SSE_DONE_STRING
from litellm.litellm_core_utils.asyncify import run_async_function from litellm.litellm_core_utils.asyncify import run_async_function
from litellm.litellm_core_utils.core_helpers import process_response_headers from litellm.litellm_core_utils.core_helpers import process_response_headers, require_attributes
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.litellm_core_utils.llm_response_utils.get_api_base import get_api_base from litellm.litellm_core_utils.llm_response_utils.get_api_base import get_api_base
from litellm.litellm_core_utils.thread_pool_executor import executor from litellm.litellm_core_utils.thread_pool_executor import executor
@ -32,6 +33,10 @@ _INTERACTIONS_STREAMING_CHUNK_ADAPTER: Final = TypeAdapter(
) )
class _SupportsGet(Protocol):
def get(self, key: str, default: object, /) -> object: ...
class BaseInteractionsAPIStreamingIterator: class BaseInteractionsAPIStreamingIterator:
""" """
Base class for streaming iterators that process responses from the Interactions API. Base class for streaming iterators that process responses from the Interactions API.
@ -45,7 +50,7 @@ class BaseInteractionsAPIStreamingIterator:
model: str | None, model: str | None,
interactions_api_config: BaseInteractionsAPIConfig, interactions_api_config: BaseInteractionsAPIConfig,
logging_obj: LiteLLMLoggingObj, logging_obj: LiteLLMLoggingObj,
litellm_metadata: dict[str, Any] | None = None, litellm_metadata: Mapping[str, object] | None = None,
custom_llm_provider: str | None = None, custom_llm_provider: str | None = None,
): ):
self.response = response self.response = response
@ -65,9 +70,9 @@ class BaseInteractionsAPIStreamingIterator:
model=model or "", model=model or "",
optional_params=self.logging_obj.model_call_details.get("litellm_params", {}), optional_params=self.logging_obj.model_call_details.get("litellm_params", {}),
) )
_model_info: Final[dict] = litellm_metadata.get("model_info", {}) if litellm_metadata else {} _model_info: Final[object] = litellm_metadata.get("model_info", {}) if litellm_metadata else {}
self._hidden_params = { self._hidden_params = {
"model_id": _model_info.get("id", None), "model_id": require_attributes(_model_info, _SupportsGet, "get").get("id", None),
"api_base": _api_base, "api_base": _api_base,
} }
self._hidden_params["additional_headers"] = process_response_headers(self.response.headers or {}) self._hidden_params["additional_headers"] = process_response_headers(self.response.headers or {})
@ -135,7 +140,7 @@ class InteractionsAPIStreamingIterator(BaseInteractionsAPIStreamingIterator):
model: str | None, model: str | None,
interactions_api_config: BaseInteractionsAPIConfig, interactions_api_config: BaseInteractionsAPIConfig,
logging_obj: LiteLLMLoggingObj, logging_obj: LiteLLMLoggingObj,
litellm_metadata: dict[str, Any] | None = None, litellm_metadata: Mapping[str, object] | None = None,
custom_llm_provider: str | None = None, custom_llm_provider: str | None = None,
): ):
super().__init__( super().__init__(
@ -202,7 +207,7 @@ class SyncInteractionsAPIStreamingIterator(BaseInteractionsAPIStreamingIterator)
model: str | None, model: str | None,
interactions_api_config: BaseInteractionsAPIConfig, interactions_api_config: BaseInteractionsAPIConfig,
logging_obj: LiteLLMLoggingObj, logging_obj: LiteLLMLoggingObj,
litellm_metadata: dict[str, Any] | None = None, litellm_metadata: Mapping[str, object] | None = None,
custom_llm_provider: str | None = None, custom_llm_provider: str | None = None,
): ):
super().__init__( super().__init__(

View file

@ -313,7 +313,7 @@ def calculate_request_duration(file: FileTypes) -> float | None:
file_content = content file_content = content
elif hasattr(content, "read") and not isinstance(content, (str, os.PathLike)): elif hasattr(content, "read") and not isinstance(content, (str, os.PathLike)):
# File-like object in tuple # File-like object in tuple
current_pos: Final = getattr(content, "tell", lambda: None)() current_pos: Final = content.tell() if hasattr(content, "tell") else None
# Seek to start to ensure we read the entire content # Seek to start to ensure we read the entire content
if hasattr(content, "seek"): if hasattr(content, "seek"):
content.seek(0) content.seek(0)

View file

@ -4,6 +4,7 @@ import copy
import logging import logging
import math import math
import re import re
import sys
from collections.abc import Collection, Iterable, Iterator, Mapping, Sequence from collections.abc import Collection, Iterable, Iterator, Mapping, Sequence
from types import MappingProxyType from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Final, Literal, Protocol, TypeVar, cast, runtime_checkable from typing import TYPE_CHECKING, Any, Final, Literal, Protocol, TypeVar, cast, runtime_checkable
@ -133,7 +134,7 @@ class _SupportsIter(Protocol):
@runtime_checkable @runtime_checkable
class _SupportsGetItem(Protocol): class _SupportsGetItem(Protocol):
def __getitem__(self, index: int, /) -> object: ... def __getitem__(self, key: str | int, /) -> object: ...
def attribute_or_none(source: object, name: str) -> object: def attribute_or_none(source: object, name: str) -> object:
@ -166,6 +167,28 @@ def iter_items(value: object) -> Iterable[object]:
raise TypeError(f"'{type(value).__name__}' object is not iterable") raise TypeError(f"'{type(value).__name__}' object is not iterable")
@runtime_checkable
class _SupportsContains(Protocol):
def __contains__(self, key: object, /) -> bool: ...
_NOT_A_CONTAINER: Final = "is not a container or iterable" if sys.version_info >= (3, 14) else "is not iterable"
def json_contains(value: object, key: object) -> bool:
if isinstance(value, _SupportsContains):
return key in value
if isinstance(value, (_SupportsIter, _SupportsGetItem)):
return key in tuple(iter_items(value))
raise TypeError(f"argument of type '{type(value).__name__}' {_NOT_A_CONTAINER}")
def subscript(value: object, key: str | int) -> object:
if isinstance(value, _SupportsGetItem):
return value[key]
raise TypeError(f"'{type(value).__name__}' object is not subscriptable")
def attribute_items(source: object, name: str) -> Iterable[object]: def attribute_items(source: object, name: str) -> Iterable[object]:
value: Final = attribute_or_none(source, name) value: Final = attribute_or_none(source, name)
return iter_items(value) if value else () return iter_items(value) if value else ()

View file

@ -5,8 +5,9 @@ If the ddtrace package is not installed, the tracer will be a no-op.
""" """
from contextlib import contextmanager from contextlib import contextmanager
from typing import TYPE_CHECKING, Any, Final from typing import TYPE_CHECKING, Any, Final, Protocol
from litellm.litellm_core_utils.core_helpers import has_attributes
from litellm.secret_managers.main import get_secret_bool from litellm.secret_managers.main import get_secret_bool
if TYPE_CHECKING: if TYPE_CHECKING:
@ -15,6 +16,14 @@ else:
DD_TRACER = Any DD_TRACER = Any
class _TagStrSpan(Protocol):
def set_tag_str(self, key: str, value: str, /) -> object: ...
class _TagSpan(Protocol):
def set_tag(self, key: str, value: str, /) -> object: ...
class NullSpan: class NullSpan:
"""A no-op span implementation.""" """A no-op span implementation."""
@ -81,7 +90,7 @@ else:
tracer = NullTracer() tracer = NullTracer()
def get_active_span() -> Any | None: def get_active_span() -> object:
""" """
Return the active Datadog span, checking current span first and then root span. Return the active Datadog span, checking current span first and then root span.
""" """
@ -115,10 +124,10 @@ def set_active_span_tag(tag_key: str, tag_value: str) -> bool:
return False return False
try: try:
if hasattr(span, "set_tag_str"): if has_attributes(span, _TagStrSpan, "set_tag_str"):
span.set_tag_str(tag_key, str(tag_value)) span.set_tag_str(tag_key, str(tag_value))
return True return True
if hasattr(span, "set_tag"): if has_attributes(span, _TagSpan, "set_tag"):
span.set_tag(tag_key, str(tag_value)) span.set_tag(tag_key, str(tag_value))
return True return True
except Exception: except Exception:

View file

@ -19,7 +19,7 @@ from datetime import datetime as dt_object
from enum import Enum from enum import Enum
from functools import lru_cache from functools import lru_cache
from types import MappingProxyType, TracebackType from types import MappingProxyType, TracebackType
from typing import TYPE_CHECKING, Any, Final, Literal, NamedTuple, TypeVar, cast from typing import TYPE_CHECKING, Any, Final, Literal, NamedTuple, Protocol, TypeVar, cast
from httpx import Response, Timeout from httpx import Response, Timeout
from openai import AsyncStream as OpenAIAsyncStream from openai import AsyncStream as OpenAIAsyncStream
@ -90,6 +90,7 @@ from litellm.litellm_core_utils.classifier_logging import (
is_classifier_call, is_classifier_call,
) )
from litellm.litellm_core_utils.core_helpers import ( from litellm.litellm_core_utils.core_helpers import (
as_str_mapping,
finite_cost, finite_cost,
get_provider_response_headers_from_hidden_params, get_provider_response_headers_from_hidden_params,
is_expected_client_error, is_expected_client_error,
@ -99,6 +100,7 @@ from litellm.litellm_core_utils.core_helpers import (
proxy_stamped_used_client_oauth_token, proxy_stamped_used_client_oauth_token,
reconstruct_model_name, reconstruct_model_name,
reports_native_messages_cost, reports_native_messages_cost,
require_attributes,
set_response_cost_in_hidden_params, set_response_cost_in_hidden_params,
single_log_line, single_log_line,
) )
@ -666,6 +668,31 @@ class UsageObservation(Enum):
ESTIMATED = "estimated" ESTIMATED = "estimated"
class _A2AUsageSlot(Protocol):
usage: Mapping[str, object] | None
class _A2AResponseCopier(Protocol):
model_copy: Callable[..., _A2AUsageSlot]
class _MetadataLookup(Protocol):
get: Callable[[str], object]
class _ParamsLookup(Protocol):
def get(self, key: str, default: object, /) -> object: ...
def _callback_control_params(litellm_params: object) -> dict[str, object]:
mapping: Final = as_str_mapping(litellm_params)
if isinstance(mapping, dict):
return mapping
if mapping is not None:
return dict(mapping)
return litellm_params.model_dump() if isinstance(litellm_params, BaseModel) else {}
class Logging(LiteLLMLoggingBaseClass): class Logging(LiteLLMLoggingBaseClass):
global \ global \
supabaseClient, \ supabaseClient, \
@ -929,7 +956,9 @@ class Logging(LiteLLMLoggingBaseClass):
new_call.start_time = start_time new_call.start_time = start_time
new_call.litellm_params = litellm_params new_call.litellm_params = litellm_params
new_call.model_call_details = { new_call.model_call_details = {
**{key: value for key, value in self.model_call_details.items() if not key.startswith("_accounting_")}, **{
key: value for key, value in self._model_call_details_view.items() if not key.startswith("_accounting_")
},
"litellm_call_id": litellm_call_id, "litellm_call_id": litellm_call_id,
"start_time": start_time, "start_time": start_time,
"first_api_call_start_time": None, "first_api_call_start_time": None,
@ -2467,7 +2496,7 @@ class Logging(LiteLLMLoggingBaseClass):
return self.response_cost_calculator(result=result, cache_hit=cache_hit) return self.response_cost_calculator(result=result, cache_hit=cache_hit)
@staticmethod @staticmethod
def is_sync_litellm_request(litellm_params: Mapping[str, object]) -> bool: def is_sync_litellm_request(litellm_params: _ParamsLookup) -> bool:
"""True for sync SDK entrypoints (``completion``), false for async (``acompletion``, etc.).""" """True for sync SDK entrypoints (``completion``), false for async (``acompletion``, etc.)."""
return ( return (
litellm_params.get(CallTypes.acompletion.value, False) is not True litellm_params.get(CallTypes.acompletion.value, False) is not True
@ -2918,11 +2947,12 @@ class Logging(LiteLLMLoggingBaseClass):
"""Deprecated alias of mark_logging_complete, kept for callers of the old name""" """Deprecated alias of mark_logging_complete, kept for callers of the old name"""
self.mark_logging_complete(event_type=event_type) self.mark_logging_complete(event_type=event_type)
def should_run_callback(self, callback: litellm.CALLBACK_TYPES, litellm_params: dict, event_hook: str) -> bool: def should_run_callback(self, callback: litellm.CALLBACK_TYPES, litellm_params: object, event_hook: str) -> bool:
if litellm.global_disable_no_log_param: if litellm.global_disable_no_log_param:
return True return True
if litellm_params.get("no-log", False) is True: request_params: Final = require_attributes(litellm_params, _ParamsLookup, "get")
if request_params.get("no-log", False) is True:
# proxy cost tracking cal backs should run # proxy cost tracking cal backs should run
if not (isinstance(callback, CustomLogger) and "_PROXY_" in callback.__class__.__name__): if not (isinstance(callback, CustomLogger) and "_PROXY_" in callback.__class__.__name__):
@ -2932,7 +2962,7 @@ class Logging(LiteLLMLoggingBaseClass):
# Check for dynamically disabled callbacks via headers # Check for dynamically disabled callbacks via headers
if EnterpriseCallbackControls is not None and EnterpriseCallbackControls.is_callback_disabled_dynamically( if EnterpriseCallbackControls is not None and EnterpriseCallbackControls.is_callback_disabled_dynamically(
callback=callback, callback=callback,
litellm_params=litellm_params, litellm_params=_callback_control_params(litellm_params),
standard_callback_dynamic_params=self.standard_callback_dynamic_params, standard_callback_dynamic_params=self.standard_callback_dynamic_params,
): ):
verbose_logger.debug( verbose_logger.debug(
@ -3643,8 +3673,8 @@ class Logging(LiteLLMLoggingBaseClass):
build_logging_payload=True, build_logging_payload=True,
publish_logging_payload=not for_async_request, publish_logging_payload=not for_async_request,
) )
litellm_params = self.model_call_details.get("litellm_params", {}) litellm_params = self._model_call_details_view.get("litellm_params", {})
is_sync_request: Final = self.is_sync_litellm_request(litellm_params) is_sync_request: Final = self.is_sync_litellm_request(require_attributes(litellm_params, _ParamsLookup, "get"))
try: try:
## BUILD COMPLETE STREAMED RESPONSE ## BUILD COMPLETE STREAMED RESPONSE
complete_streaming_response: ( complete_streaming_response: (
@ -3800,18 +3830,23 @@ class Logging(LiteLLMLoggingBaseClass):
model = call_details_view["model"] model = call_details_view["model"]
messages = call_details_view["messages"] messages = call_details_view["messages"]
optional_params = kwargs.get("optional_params", {}) optional_params = call_details_view.get("optional_params", {})
litellm_params = kwargs.get("litellm_params", {}) litellm_params = call_details_view.get("litellm_params", {})
supabaseClient.log_event( supabaseClient.log_event(
model=model, model=model,
messages=messages, messages=messages,
end_user=optional_params.get("user", "default"), end_user=require_attributes(optional_params, _ParamsLookup, "get").get("user", "default"),
response_obj=result, response_obj=result,
start_time=start_time, start_time=start_time,
end_time=end_time, end_time=end_time,
litellm_call_id=( litellm_call_id=(
current_call_id current_call_id
if (current_call_id := litellm_params.get("litellm_call_id")) is not None if (
current_call_id := require_attributes(litellm_params, _ParamsLookup, "get").get(
"litellm_call_id", None
)
)
is not None
else str(uuid.uuid4()) else str(uuid.uuid4())
), ),
print_verbose=print_verbose, print_verbose=print_verbose,
@ -4369,7 +4404,7 @@ class Logging(LiteLLMLoggingBaseClass):
for callback in delivery_callbacks: for callback in delivery_callbacks:
# check if callback can run for this request # check if callback can run for this request
litellm_params = self.model_call_details.get("litellm_params", {}) litellm_params = self._model_call_details_view.get("litellm_params", {})
should_run = self.should_run_callback( should_run = self.should_run_callback(
callback=callback, callback=callback,
litellm_params=litellm_params, litellm_params=litellm_params,
@ -4658,8 +4693,8 @@ class Logging(LiteLLMLoggingBaseClass):
verbose_logger.debug("Logging Details LiteLLM-Failure Call: %s", litellm.failure_callback) verbose_logger.debug("Logging Details LiteLLM-Failure Call: %s", litellm.failure_callback)
if not self.should_run_logging(event_type="sync_failure"): # prevent double logging if not self.should_run_logging(event_type="sync_failure"): # prevent double logging
return return
litellm_params: Final = self.model_call_details.get("litellm_params", {}) litellm_params: Final = self._model_call_details_view.get("litellm_params", {})
is_sync_request: Final = self.is_sync_litellm_request(litellm_params) is_sync_request: Final = self.is_sync_litellm_request(require_attributes(litellm_params, _ParamsLookup, "get"))
try: try:
start_time, end_time = self._failure_handler_helper_fn( start_time, end_time = self._failure_handler_helper_fn(
@ -4879,7 +4914,7 @@ class Logging(LiteLLMLoggingBaseClass):
self.mark_logging_complete(event_type="async_failure") self.mark_logging_complete(event_type="async_failure")
for callback in callbacks: for callback in callbacks:
try: try:
litellm_params = self.model_call_details.get("litellm_params", {}) litellm_params = self._model_call_details_view.get("litellm_params", {})
should_run = self.should_run_callback( should_run = self.should_run_callback(
callback=callback, callback=callback,
litellm_params=litellm_params, litellm_params=litellm_params,
@ -5441,7 +5476,7 @@ class Logging(LiteLLMLoggingBaseClass):
httpx_response: Final[Response | None] = self.model_call_details.get("httpx_response", None) httpx_response: Final[Response | None] = self.model_call_details.get("httpx_response", None)
if httpx_response is None: if httpx_response is None:
raise ValueError("Google GenAI Generate Content: httpx_response is None") raise ValueError("Google GenAI Generate Content: httpx_response is None")
dict_result: Final = httpx_response.json() dict_result: Final[GenerateContentResponseBody] = httpx_response.json()
result = litellm.VertexGeminiConfig().transform_google_generate_content_to_openai_model_response( result = litellm.VertexGeminiConfig().transform_google_generate_content_to_openai_model_response(
completion_response=dict_result, completion_response=dict_result,
model_response=litellm.ModelResponse(), model_response=litellm.ModelResponse(),
@ -5454,7 +5489,7 @@ class Logging(LiteLLMLoggingBaseClass):
) )
return result return result
def _handle_a2a_response_logging(self, result: Any) -> object: def _handle_a2a_response_logging(self, result: object) -> object:
""" """
Handles logging for A2A (Agent-to-Agent) responses. Handles logging for A2A (Agent-to-Agent) responses.
@ -5473,7 +5508,7 @@ class Logging(LiteLLMLoggingBaseClass):
return result return result
# Deep copy result and add usage # Deep copy result and add usage
result_copy: Final = result.model_copy(deep=True) result_copy: Final = require_attributes(result, _A2AResponseCopier, "model_copy").model_copy(deep=True)
result_copy.usage = usage.model_dump() if hasattr(usage, "model_dump") else dict(usage) result_copy.usage = usage.model_dump() if hasattr(usage, "model_dump") else dict(usage)
return result_copy return result_copy
@ -7475,7 +7510,7 @@ class StandardLoggingPayloadSetup:
@staticmethod @staticmethod
def get_standard_logging_payload_trace_id( def get_standard_logging_payload_trace_id(
logging_obj: Logging, logging_obj: Logging,
litellm_params: Mapping[str, Any], litellm_params: Mapping[str, object],
) -> str: ) -> str:
""" """
Returns the `litellm_trace_id` for this request Returns the `litellm_trace_id` for this request
@ -7491,9 +7526,13 @@ class StandardLoggingPayloadSetup:
""" """
dynamic_litellm_session_id: Final = litellm_params.get("litellm_session_id") dynamic_litellm_session_id: Final = litellm_params.get("litellm_session_id")
dynamic_litellm_trace_id: Final = litellm_params.get("litellm_trace_id") dynamic_litellm_trace_id: Final = litellm_params.get("litellm_trace_id")
metadata: Final[Mapping[str, object] | None] = litellm_params.get("metadata") metadata: Final = litellm_params.get("metadata")
metadata_session_id: Final = metadata.get("session_id") if metadata else None metadata_session_id: Final = (
metadata_trace_id: Final = metadata.get("trace_id") if metadata else None require_attributes(metadata, _MetadataLookup, "get").get("session_id") if metadata else None
)
metadata_trace_id: Final = (
require_attributes(metadata, _MetadataLookup, "get").get("trace_id") if metadata else None
)
ordered_candidates: Final[tuple[object, object, object, object]] = ( ordered_candidates: Final[tuple[object, object, object, object]] = (
(dynamic_litellm_trace_id, dynamic_litellm_session_id, metadata_trace_id, metadata_session_id) (dynamic_litellm_trace_id, dynamic_litellm_session_id, metadata_trace_id, metadata_session_id)
@ -7508,7 +7547,7 @@ class StandardLoggingPayloadSetup:
@staticmethod @staticmethod
def get_standard_logging_payload_session_id( def get_standard_logging_payload_session_id(
logging_obj: Logging, logging_obj: Logging,
litellm_params: Mapping[str, Any], litellm_params: Mapping[str, object],
) -> str: ) -> str:
""" """
Returns the end-user/conversation `litellm_session_id` for this request, independent of trace_id. Returns the end-user/conversation `litellm_session_id` for this request, independent of trace_id.
@ -7523,8 +7562,10 @@ class StandardLoggingPayloadSetup:
dynamic_litellm_session_id: Final[object] = litellm_params.get("litellm_session_id") dynamic_litellm_session_id: Final[object] = litellm_params.get("litellm_session_id")
if dynamic_litellm_session_id: if dynamic_litellm_session_id:
return str(dynamic_litellm_session_id) return str(dynamic_litellm_session_id)
metadata: Final[Mapping[str, object] | None] = litellm_params.get("metadata") metadata: Final = litellm_params.get("metadata")
metadata_session_id: Final = metadata.get("session_id") if metadata else None metadata_session_id: Final = (
require_attributes(metadata, _MetadataLookup, "get").get("session_id") if metadata else None
)
if metadata_session_id: if metadata_session_id:
return str(metadata_session_id) return str(metadata_session_id)
return logging_obj.litellm_session_id return logging_obj.litellm_session_id

View file

@ -1,7 +1,8 @@
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from types import MappingProxyType from types import MappingProxyType
from typing import Any, Final from typing import Any, Final, Protocol
from litellm.litellm_core_utils.core_helpers import iter_items, require_attributes
from litellm.types.utils import ( from litellm.types.utils import (
CompletionTokensDetailsWrapper, CompletionTokensDetailsWrapper,
PromptTokensDetailsWrapper, PromptTokensDetailsWrapper,
@ -55,19 +56,29 @@ _INTERACTIONS_MODALITY_FIELDS: Final[Mapping[str, str]] = MappingProxyType(
) )
def _modality_field(entry: Mapping[str, object]) -> str | None: class _SupportsGet(Protocol):
return _INTERACTIONS_MODALITY_FIELDS.get(str(entry.get("modality", "")).lower()) def get(self, key: str, default: object = None, /) -> object: ...
def _modality_field(entry: object) -> str | None:
return _INTERACTIONS_MODALITY_FIELDS.get(
str(require_attributes(entry, _SupportsGet, "get").get("modality", "")).lower()
)
def _token_count(value: object) -> int: def _token_count(value: object) -> int:
return value if isinstance(value, int) else 0 return value if isinstance(value, int) else 0
def _modality_token_sums(entries: Sequence[Mapping[str, object]]) -> Mapping[str, int]: def _modality_token_sums(entries: Sequence[object]) -> Mapping[str, int]:
fields: Final = frozenset(field for entry in entries if (field := _modality_field(entry)) is not None) fields: Final = frozenset(field for entry in entries if (field := _modality_field(entry)) is not None)
return MappingProxyType( return MappingProxyType(
{ {
field: sum(_token_count(entry.get("tokens")) for entry in entries if _modality_field(entry) == field) field: sum(
_token_count(require_attributes(entry, _SupportsGet, "get").get("tokens"))
for entry in entries
if _modality_field(entry) == field
)
for field in fields for field in fields
} }
) )
@ -119,12 +130,16 @@ class InteractionsUsageObjectTransformation:
return "total_input_tokens" in usage_object or "total_output_tokens" in usage_object return "total_input_tokens" in usage_object or "total_output_tokens" in usage_object
@staticmethod @staticmethod
def transform_interactions_usage_object(usage_object: Mapping[str, Any]) -> Usage: def transform_interactions_usage_object(usage_object: Mapping[str, object]) -> Usage:
input_entries: Final = tuple(usage_object.get("input_tokens_by_modality") or ()) + tuple( input_entries: Final = tuple(iter_items(usage_object.get("input_tokens_by_modality") or ())) + tuple(
usage_object.get("tool_use_tokens_by_modality") or () iter_items(usage_object.get("tool_use_tokens_by_modality") or ())
)
cached_sums: Final = _modality_token_sums(
tuple(iter_items(usage_object.get("cached_tokens_by_modality") or ()))
)
output_sums: Final = _modality_token_sums(
tuple(iter_items(usage_object.get("output_tokens_by_modality") or ()))
) )
cached_sums: Final = _modality_token_sums(tuple(usage_object.get("cached_tokens_by_modality") or ()))
output_sums: Final = _modality_token_sums(tuple(usage_object.get("output_tokens_by_modality") or ()))
total_cached_tokens: Final = _token_count(usage_object.get("total_cached_tokens")) total_cached_tokens: Final = _token_count(usage_object.get("total_cached_tokens"))
input_sums: Final = _subtract_cached_from_input( input_sums: Final = _subtract_cached_from_input(

View file

@ -6,10 +6,10 @@ from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field, replace from dataclasses import dataclass, field, replace
from datetime import datetime, timezone, tzinfo from datetime import datetime, timezone, tzinfo
from types import MappingProxyType from types import MappingProxyType
from typing import Final, Literal, Protocol, TypedDict, cast from typing import Final, Literal, Protocol, SupportsFloat, SupportsIndex, TypedDict, cast
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from typing_extensions import ReadOnly from typing_extensions import Buffer, ReadOnly
import litellm import litellm
from litellm._internal_context import current_billing_time from litellm._internal_context import current_billing_time
@ -48,7 +48,6 @@ _IMAGE_RESPONSE_CALL_TYPES: Final = frozenset(
} }
) )
# Pre-resolved DataResidency enum values for fast membership checks
_VALID_DATA_RESIDENCIES: Final = frozenset(r.value for r in DataResidency) _VALID_DATA_RESIDENCIES: Final = frozenset(r.value for r in DataResidency)
_DEPLOYMENT_PRICING_KEYS: Final[frozenset[str]] = frozenset(CustomPricingLiteLLMParams.model_fields) _DEPLOYMENT_PRICING_KEYS: Final[frozenset[str]] = frozenset(CustomPricingLiteLLMParams.model_fields)
@ -1428,6 +1427,13 @@ def get_regional_uplift_multiplier(model_info: ModelInfo, data_residency: str |
multiplier: Final = model_info.get(f"regional_processing_uplift_multiplier_{residency}") multiplier: Final = model_info.get(f"regional_processing_uplift_multiplier_{residency}")
if multiplier is None: if multiplier is None:
return 1.0 return 1.0
if not isinstance(multiplier, (str, Buffer, SupportsFloat, SupportsIndex)):
verbose_logger.error(
"Invalid regional_processing_uplift_multiplier_%s for model (got %s); defaulting to 1.0",
residency,
type(multiplier).__name__,
)
return 1.0
try: try:
return float(multiplier) return float(multiplier)
except (TypeError, ValueError): except (TypeError, ValueError):

View file

@ -6,7 +6,7 @@ import json
from collections.abc import Hashable, Iterable, Mapping, Sequence from collections.abc import Hashable, Iterable, Mapping, Sequence
from dataclasses import dataclass from dataclasses import dataclass
from itertools import chain, groupby from itertools import chain, groupby
from typing import TYPE_CHECKING, Any, Final, Literal, TypeAlias, cast from typing import TYPE_CHECKING, Final, Literal, TypeAlias, cast
from pydantic import TypeAdapter from pydantic import TypeAdapter
@ -1143,7 +1143,7 @@ def strip_prefill_prefix_field(message: AllMessageValues) -> AllMessageValues:
return _message_without_fields(message, frozenset({"prefix"})) return _message_without_fields(message, frozenset({"prefix"}))
def filter_value_from_dict(dictionary: dict, key: str, depth: int = 0) -> Any: def filter_value_from_dict(dictionary: dict, key: str, depth: int = 0) -> Mapping[object, object]:
""" """
Filters a value from a dictionary Filters a value from a dictionary

View file

@ -10,7 +10,7 @@ from collections.abc import Callable, Container, Iterable, Iterator, Mapping, Se
from dataclasses import dataclass, replace from dataclasses import dataclass, replace
from enum import Enum from enum import Enum
from types import MappingProxyType from types import MappingProxyType
from typing import Annotated, Any, Final, TypeAlias, TypedDict, TypeVar, cast, overload from typing import Annotated, Any, Final, Protocol, TypeAlias, TypedDict, TypeVar, cast, overload
from jinja2.sandbox import ImmutableSandboxedEnvironment from jinja2.sandbox import ImmutableSandboxedEnvironment
from pydantic import BaseModel, BeforeValidator, ConfigDict, JsonValue, TypeAdapter, ValidationError, model_validator from pydantic import BaseModel, BeforeValidator, ConfigDict, JsonValue, TypeAdapter, ValidationError, model_validator
@ -22,7 +22,7 @@ import litellm.types.llms
from litellm import verbose_logger from litellm import verbose_logger
from litellm._uuid import uuid from litellm._uuid import uuid
from litellm.constants import REDACTED_BY_LITELLM from litellm.constants import REDACTED_BY_LITELLM
from litellm.litellm_core_utils.core_helpers import as_str_mapping, attribute_or_none from litellm.litellm_core_utils.core_helpers import as_str_mapping, attribute_or_none, require_attributes
from litellm.litellm_core_utils.prompt_templates.mid_conversation_system import ( from litellm.litellm_core_utils.prompt_templates.mid_conversation_system import (
anthropic_system_messages, anthropic_system_messages,
find_anthropic_server_tool_result, find_anthropic_server_tool_result,
@ -4150,7 +4150,7 @@ class BedrockImageProcessor:
_validate_format = validate_format _validate_format = validate_format
@staticmethod @staticmethod
def _get_document_format(mime_type: str, supported_doc_formats: list[str]) -> str: def _get_document_format(mime_type: str, supported_doc_formats: Sequence[str]) -> str:
""" """
Get the document format from the mime type Get the document format from the mime type
@ -6571,6 +6571,20 @@ def has_tool_with_name(tools: object, tool_name: str) -> bool:
return False return False
class _SupportsModelDump(Protocol):
def model_dump(self) -> dict[str, object]: ...
class _StructuredMessagesSource(Protocol):
def get_structured_messages(self, data: dict[str, object], /) -> Sequence[object] | None: ...
def _instantiate_structured_message_sources(
source_classes: Iterable[Callable[[], _StructuredMessagesSource]],
) -> Sequence[_StructuredMessagesSource]:
return [source_cls() for source_cls in source_classes]
def resolve_structured_messages( def resolve_structured_messages(
messages: list[dict[str, object]] | None, messages: list[dict[str, object]] | None,
request_kwargs: dict[str, Any], request_kwargs: dict[str, Any],
@ -6608,16 +6622,17 @@ def resolve_structured_messages(
break break
# 2. Fallback: try each mapped handler until one produces messages # 2. Fallback: try each mapped handler until one produces messages
handlers_to_try: Final[list[Any]] = [] handlers_to_try: Final = _instantiate_structured_message_sources(
if call_type is not None and call_type in mappings: (mappings[call_type],) if call_type is not None and call_type in mappings else mappings.values()
handlers_to_try.append(mappings[call_type]()) )
else:
handlers_to_try.extend(handler_cls() for handler_cls in mappings.values())
for handler in handlers_to_try: for handler in handlers_to_try:
structured = handler.get_structured_messages(request_kwargs) structured = handler.get_structured_messages(request_kwargs)
if structured: if structured:
return [msg if isinstance(msg, dict) else msg.model_dump() for msg in structured] return [
msg if isinstance(msg, dict) else require_attributes(msg, _SupportsModelDump, "model_dump").model_dump()
for msg in structured
]
return None return None

View file

@ -12,7 +12,7 @@ import copy
import inspect import inspect
from collections.abc import Iterable, Mapping, Sequence from collections.abc import Iterable, Mapping, Sequence
from dataclasses import replace from dataclasses import replace
from typing import TYPE_CHECKING, Any, Final from typing import TYPE_CHECKING, Any, Final, Protocol
from pydantic import BaseModel, JsonValue, TypeAdapter, ValidationError from pydantic import BaseModel, JsonValue, TypeAdapter, ValidationError
@ -21,7 +21,9 @@ from litellm.constants import REDACTED_BY_LITELLM
from litellm.integrations.custom_logger import CustomLogger from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.classifier_logging import without_classifier_audit from litellm.litellm_core_utils.classifier_logging import without_classifier_audit
from litellm.litellm_core_utils.core_helpers import ( from litellm.litellm_core_utils.core_helpers import (
attribute_or_none,
get_metadata_variable_name_from_kwargs, get_metadata_variable_name_from_kwargs,
has_attributes,
) )
from litellm.litellm_core_utils.served_output_texts import SERVED_OUTPUT_TEXTS_KEY from litellm.litellm_core_utils.served_output_texts import SERVED_OUTPUT_TEXTS_KEY
from litellm.llms.base_llm.guardrail_translation.utils import stream_item_field, stream_item_items from litellm.llms.base_llm.guardrail_translation.utils import stream_item_field, stream_item_items
@ -42,6 +44,11 @@ if TYPE_CHECKING:
else: else:
LiteLLMLoggingObject = Any LiteLLMLoggingObject = Any
class _HasArguments(Protocol):
arguments: str
MESSAGE_REDACTION_ENABLE_HEADERS: Final = ("litellm-enable-message-redaction", "x-litellm-enable-message-redaction") MESSAGE_REDACTION_ENABLE_HEADERS: Final = ("litellm-enable-message-redaction", "x-litellm-enable-message-redaction")
@ -163,7 +170,7 @@ def _redact_bedrock_guardrail_trace(response: object) -> None:
} }
response.update(traces) response.update(traces)
return return
for field, value in ((name, getattr(response, name, None)) for name in BEDROCK_GUARDRAIL_TRACE_FIELDS): for field, value in ((name, attribute_or_none(response, name)) for name in BEDROCK_GUARDRAIL_TRACE_FIELDS):
if value is not None: if value is not None:
setattr(response, field, _redacted_bedrock_trace(value)) setattr(response, field, _redacted_bedrock_trace(value))
@ -191,8 +198,8 @@ def _redact_tool_calls(tool_calls) -> None:
if not tool_calls: if not tool_calls:
return return
for tool_call in tool_calls: for tool_call in tool_calls:
function = getattr(tool_call, "function", None) function = attribute_or_none(tool_call, "function")
if function is not None and hasattr(function, "arguments"): if function is not None and has_attributes(function, _HasArguments, "arguments"):
function.arguments = REDACTED_BY_LITELLM function.arguments = REDACTED_BY_LITELLM

View file

@ -7,7 +7,7 @@ import logging
import threading import threading
import time import time
import traceback import traceback
from collections.abc import AsyncIterator, Callable, Iterable, Iterator, Mapping, Sequence from collections.abc import AsyncIterator, Callable, Iterable, Iterator, Mapping, Sequence, Sized
from dataclasses import dataclass from dataclasses import dataclass
from types import MappingProxyType from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Final, NoReturn, Protocol, TypeVar, cast from typing import TYPE_CHECKING, Any, Final, NoReturn, Protocol, TypeVar, cast
@ -17,7 +17,7 @@ import httpx
from openai.types.chat import ChatCompletionChunk from openai.types.chat import ChatCompletionChunk
from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice
from openai.types.chat.chat_completion_chunk import ChoiceDelta from openai.types.chat.chat_completion_chunk import ChoiceDelta
from pydantic import BaseModel, ValidationError from pydantic import BaseModel, TypeAdapter, ValidationError
from typing_extensions import NotRequired, TypedDict from typing_extensions import NotRequired, TypedDict
import litellm import litellm
@ -27,7 +27,9 @@ from litellm.constants import STREAM_CHUNK_CHOICE_FIELDS, STREAM_FINISH_CHOICE_F
from litellm.litellm_core_utils.asyncify import asyncify from litellm.litellm_core_utils.asyncify import asyncify
from litellm.litellm_core_utils.core_helpers import ( from litellm.litellm_core_utils.core_helpers import (
UPSTREAM_USAGE_COST, UPSTREAM_USAGE_COST,
attribute_or_none,
chunk_usage, chunk_usage,
decoded_json_object,
provider_reported_usage_cost, provider_reported_usage_cost,
stamp_litellm_bill_on_usage, stamp_litellm_bill_on_usage,
) )
@ -129,6 +131,13 @@ def _apply_provider_fields(response: ModelResponseStream, fields: Mapping[str, o
setattr(response, key, value) setattr(response, key, value)
def _entry_len(entries: Mapping[str, object], key: str) -> int:
value: Final = entries[key]
if isinstance(value, Sized):
return len(value)
raise TypeError(f"object of type '{type(value).__name__}' has no len()")
def _prepend_upstream_choice_extras( def _prepend_upstream_choice_extras(
choice: StreamingChoices, upstream_choice: ChunkChoice, extra_keys: frozenset[str] choice: StreamingChoices, upstream_choice: ChunkChoice, extra_keys: frozenset[str]
) -> None: ) -> None:
@ -346,6 +355,9 @@ class _ParsedChunkHiddenParams(LiteLLMBaseModel):
upstream_provider: str | None = None upstream_provider: str | None = None
_DECODED_JSON: Final = TypeAdapter(object)
def _provider_response_model(chunk: object) -> str | None: def _provider_response_model(chunk: object) -> str | None:
model: Final[object] = chunk.get("model") if isinstance(chunk, Mapping) else getattr(chunk, "model", None) model: Final[object] = chunk.get("model") if isinstance(chunk, Mapping) else getattr(chunk, "model", None)
return model if isinstance(model, str) and model else None return model if isinstance(model, str) and model else None
@ -432,9 +444,8 @@ class CustomStreamWrapper:
self._closed = False self._closed = False
self._stream_created_time: float = time.time() self._stream_created_time: float = time.time()
litellm_params: Final[GenericLiteLLMParams] = GenericLiteLLMParams.model_validate( raw_litellm_params: Final = dict[str, object](**self.logging_obj.model_call_details.get("litellm_params", {}))
dict(**self.logging_obj.model_call_details.get("litellm_params", {})) litellm_params: Final[GenericLiteLLMParams] = GenericLiteLLMParams.model_validate(raw_litellm_params)
)
self.merge_reasoning_content_in_choices: bool = litellm_params.merge_reasoning_content_in_choices or False self.merge_reasoning_content_in_choices: bool = litellm_params.merge_reasoning_content_in_choices or False
self.sent_first_thinking_block = False self.sent_first_thinking_block = False
self.sent_last_thinking_block = False self.sent_last_thinking_block = False
@ -465,7 +476,7 @@ class CustomStreamWrapper:
_api_base: Final = get_api_base( _api_base: Final = get_api_base(
model=model or "", model=model or "",
optional_params=self.logging_obj.model_call_details.get("litellm_params", {}), optional_params=raw_litellm_params,
) )
self._hidden_params: dict[str, object] = { self._hidden_params: dict[str, object] = {
@ -953,7 +964,7 @@ class CustomStreamWrapper:
if "text_output" in chunk: if "text_output" in chunk:
response = CustomStreamWrapper.strip_sse_data_from_chunk(chunk) or "" response = CustomStreamWrapper.strip_sse_data_from_chunk(chunk) or ""
response = response.strip() response = response.strip()
parsed_response = json.loads(response) parsed_response = decoded_json_object(_DECODED_JSON.validate_python(json.loads(response)))
else: else:
return { return {
"text": "", "text": "",
@ -1087,7 +1098,7 @@ class CustomStreamWrapper:
def is_chunk_non_empty( def is_chunk_non_empty(
self, self,
completion_obj: dict[str, Any], completion_obj: Mapping[str, object],
model_response: ModelResponseStream, model_response: ModelResponseStream,
response_obj: Mapping[str, object], response_obj: Mapping[str, object],
) -> bool: ) -> bool:
@ -1097,7 +1108,7 @@ class CustomStreamWrapper:
or ( or (
"tool_calls" in completion_obj "tool_calls" in completion_obj
and completion_obj["tool_calls"] is not None and completion_obj["tool_calls"] is not None
and len(completion_obj["tool_calls"]) > 0 and _entry_len(completion_obj, "tool_calls") > 0
) )
or ("function_call" in completion_obj and completion_obj["function_call"] is not None) or ("function_call" in completion_obj and completion_obj["function_call"] is not None)
or ( or (
@ -1850,7 +1861,9 @@ class CustomStreamWrapper:
elif delta.tool_calls is not None: elif delta.tool_calls is not None:
if isinstance(delta.tool_calls, list): if isinstance(delta.tool_calls, list):
for t in delta.tool_calls: for t in delta.tool_calls:
if hasattr(t, "functions") and hasattr(getattr(t, "functions"), "arguments"): if hasattr(t, "functions") and hasattr(
attribute_or_none(t, "functions"), "arguments"
):
if t.function.arguments is None: if t.function.arguments is None:
t.function.arguments = "" t.function.arguments = ""
_json_delta: Final[_DeltaDumpDict] = delta.model_dump() _json_delta: Final[_DeltaDumpDict] = delta.model_dump()
@ -1888,7 +1901,7 @@ class CustomStreamWrapper:
return return
## CHECK FOR TOOL USE ## CHECK FOR TOOL USE
if "tool_calls" in completion_obj and len(completion_obj["tool_calls"]) > 0: if "tool_calls" in completion_obj and _entry_len(completion_obj, "tool_calls") > 0:
if self.is_function_call is True: # user passed in 'functions' param if self.is_function_call is True: # user passed in 'functions' param
completion_obj["function_call"] = completion_obj["tool_calls"][0]["function"] completion_obj["function_call"] = completion_obj["tool_calls"][0]["function"]
completion_obj["tool_calls"] = None completion_obj["tool_calls"] = None
@ -2817,8 +2830,9 @@ class CustomStreamWrapper:
def _record_and_strip_usage_chunk(self, chunk: ModelResponseStream) -> ModelResponseStream | None: def _record_and_strip_usage_chunk(self, chunk: ModelResponseStream) -> ModelResponseStream | None:
self.chunks.append(chunk.model_copy()) self.chunks.append(chunk.model_copy())
dumped_chunk: Final = MappingProxyType[str, object](chunk.model_dump())
usage_free_chunk: Final = self.model_response_creator( usage_free_chunk: Final = self.model_response_creator(
chunk={key: value for key, value in chunk.model_dump().items() if key != "usage"}, chunk={key: value for key, value in dumped_chunk.items() if key != "usage"},
hidden_params=cast( # cast-ok: preserve mapping operations on dynamic storage hidden_params=cast( # cast-ok: preserve mapping operations on dynamic storage
Mapping[str, object], getattr(chunk, HIDDEN_PARAMS_ATTR) Mapping[str, object], getattr(chunk, HIDDEN_PARAMS_ATTR)
), ),

View file

@ -11,13 +11,15 @@ A2A Protocol Format:
""" """
import json import json
from collections.abc import Sequence from collections.abc import Mapping, Sequence
from typing import TYPE_CHECKING, Any, Final, Optional from functools import reduce
from typing import TYPE_CHECKING, Any, Final, Optional, Protocol, runtime_checkable
from pydantic import ConfigDict, TypeAdapter, ValidationError from pydantic import ConfigDict, TypeAdapter, ValidationError
from typing_extensions import ReadOnly, TypedDict from typing_extensions import ReadOnly, TypedDict
from litellm._logging import verbose_proxy_logger from litellm._logging import verbose_proxy_logger
from litellm.litellm_core_utils.core_helpers import has_attributes, require_attributes, subscript
from litellm.llms.base_llm.guardrail_translation.base_translation import ( from litellm.llms.base_llm.guardrail_translation.base_translation import (
BaseTranslation, BaseTranslation,
StreamingScanKey, StreamingScanKey,
@ -30,6 +32,38 @@ if TYPE_CHECKING:
from litellm.types.proxy.auth.user_api_key_auth import UserAPIKeyAuth from litellm.types.proxy.auth.user_api_key_auth import UserAPIKeyAuth
_JSON_OBJECT: Final = TypeAdapter(dict[str, object], config=ConfigDict(strict=True, hide_input_in_errors=True)) _JSON_OBJECT: Final = TypeAdapter(dict[str, object], config=ConfigDict(strict=True, hide_input_in_errors=True))
_UNSUPPORTED_RESPONSE: Final = object()
class _ModelDumpable(Protocol):
def model_dump(self) -> object: ...
class _ResultLookup(Protocol):
def get(self, key: str, default: object, /) -> object: ...
@runtime_checkable
class _SupportsSetItem(Protocol):
def __setitem__(self, key: str, value: object, /) -> None: ...
def _item_assignment_target(value: object) -> _SupportsSetItem:
if isinstance(value, _SupportsSetItem):
return value
raise TypeError(f"'{type(value).__name__}' object does not support item assignment")
def _response_payload(response: object) -> object:
if has_attributes(response, _ModelDumpable, "model_dump"):
return response.model_dump()
if isinstance(response, dict):
return response
return _UNSUPPORTED_RESPONSE
def _path_step(current: object, key: str) -> object:
return subscript(current, int(key) if key.isdigit() else key)
class _A2ATextPart(TypedDict, total=False): class _A2ATextPart(TypedDict, total=False):
@ -123,7 +157,7 @@ class A2AGuardrailHandler(BaseTranslation):
async def process_output_response( async def process_output_response(
self, self,
response: Any, response: object,
guardrail_to_apply: "CustomGuardrail", guardrail_to_apply: "CustomGuardrail",
litellm_logging_obj: Optional["LiteLLMLoggingObj"] = None, litellm_logging_obj: Optional["LiteLLMLoggingObj"] = None,
user_api_key_dict: Optional["UserAPIKeyAuth"] = None, user_api_key_dict: Optional["UserAPIKeyAuth"] = None,
@ -147,17 +181,12 @@ class A2AGuardrailHandler(BaseTranslation):
Returns: Returns:
Modified response with guardrails applied to text content Modified response with guardrails applied to text content
""" """
# Handle both dict and Pydantic model responses payload: Final = _response_payload(response)
if hasattr(response, "model_dump"): if payload is _UNSUPPORTED_RESPONSE:
response_dict = response.model_dump()
is_pydantic = True
elif isinstance(response, dict):
response_dict = response
is_pydantic = False
else:
verbose_proxy_logger.warning("A2A: Unknown response type %s, skipping guardrail", type(response)) verbose_proxy_logger.warning("A2A: Unknown response type %s, skipping guardrail", type(response))
return response return response
response_dict: Final = require_attributes(payload, _ResultLookup, "get")
result: Final = response_dict.get("result", {}) result: Final = response_dict.get("result", {})
if not result or not isinstance(result, dict): if not result or not isinstance(result, dict):
verbose_proxy_logger.debug("A2A: No result in response, skipping guardrail") verbose_proxy_logger.debug("A2A: No result in response, skipping guardrail")
@ -218,15 +247,8 @@ class A2AGuardrailHandler(BaseTranslation):
verbose_proxy_logger.debug("A2A: Processed output response") verbose_proxy_logger.debug("A2A: Processed output response")
# Update the original response _item_assignment_target(response_dict)["result"] = result
if is_pydantic: return response
# For Pydantic models, we need to update the underlying dict
# and the model will reflect the changes
response_dict["result"] = result
return response
else:
response["result"] = result
return response
async def process_output_streaming_response( async def process_output_streaming_response(
self, self,
@ -448,20 +470,11 @@ class A2AGuardrailHandler(BaseTranslation):
def _apply_text_to_path( def _apply_text_to_path(
self, self,
result: dict[str | int, Any], result: Mapping[str, object],
path: tuple[str, ...], path: tuple[str, ...],
part_idx: int, part_idx: int,
text: str, text: str,
) -> None: ) -> None:
"""Apply guardrailed text back to the specified path in the result.""" """Apply guardrailed text back to the specified path in the result."""
# Navigate to the parts list parts: Final = reduce(_path_step, path, result)
current = result _item_assignment_target(subscript(parts, part_idx))["text"] = text
for key in path:
if key.isdigit():
# Array index
current = current[int(key)]
else:
current = current[key]
# Update the text in the part
current[part_idx]["text"] = text

View file

@ -1081,7 +1081,7 @@ class ModelResponseIterator:
if "caller" in content_block_start["content_block"]: if "caller" in content_block_start["content_block"]:
caller_data: Final = content_block_start["content_block"]["caller"] caller_data: Final = content_block_start["content_block"]["caller"]
if caller_data and tool_use is not None: if caller_data and tool_use is not None:
tool_use["caller"] = cast(dict[str, Any], caller_data) tool_use["caller"] = caller_data
elif content_block_start["content_block"]["type"] == "redacted_thinking": elif content_block_start["content_block"]["type"] == "redacted_thinking":
( (
thinking_blocks, thinking_blocks,

View file

@ -2107,7 +2107,7 @@ def _strip_empty_content_blocks_from_message(message: object) -> object:
def strip_empty_content_blocks_from_anthropic_messages( def strip_empty_content_blocks_from_anthropic_messages(
messages: Sequence[object], messages: Sequence[object],
) -> list[Any]: ) -> Sequence[object]:
""" """
Return a new message list with empty or whitespace-only ``{"type": "text"}`` Return a new message list with empty or whitespace-only ``{"type": "text"}``
content blocks and empty *unsigned* ``{"type": "thinking"}`` blocks removed. content blocks and empty *unsigned* ``{"type": "thinking"}`` blocks removed.
@ -2220,7 +2220,7 @@ def _sanitize_tool_use_id_content_block(block: object) -> object:
return block return block
def sanitize_tool_use_ids_in_anthropic_messages(messages: Sequence[object]) -> list[Any]: def sanitize_tool_use_ids_in_anthropic_messages(messages: Sequence[object]) -> Sequence[object]:
""" """
Return a new message list with ``tool_use`` / ``server_tool_use`` ``id`` and Return a new message list with ``tool_use`` / ``server_tool_use`` ``id`` and
``tool_result`` ``tool_use_id`` values rewritten to satisfy Anthropic's ``tool_result`` ``tool_use_id`` values rewritten to satisfy Anthropic's

View file

@ -17,11 +17,14 @@ execute it.
import asyncio import asyncio
import contextlib import contextlib
import json import json
from collections.abc import AsyncIterator from collections.abc import AsyncIterable, AsyncIterator
from typing import TYPE_CHECKING, Any, Final, cast from typing import TYPE_CHECKING, Any, Final, cast
from pydantic import TypeAdapter
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
from litellm.constants import STREAM_SSE_KEEPALIVE_PING_BYTES from litellm.constants import STREAM_SSE_KEEPALIVE_PING_BYTES
from litellm.litellm_core_utils.core_helpers import decoded_json_object, has_attributes
from litellm.types.integrations.custom_logger import AgenticLoopLimitError, AgenticLoopSafetyError from litellm.types.integrations.custom_logger import AgenticLoopLimitError, AgenticLoopSafetyError
if TYPE_CHECKING: if TYPE_CHECKING:
@ -42,6 +45,7 @@ AGENTIC_LOOP_LIMIT_ERROR_SSE_BYTES: Final = (
b'"The agentic loop reached its limit before this turn finished. ' b'"The agentic loop reached its limit before this turn finished. '
b'Raise max_agentic_loops to allow more loops."}}\n\n' b'Raise max_agentic_loops to allow more loops."}}\n\n'
) )
_DECODED_JSON: Final = TypeAdapter(object)
def is_server_fulfilled_tool_leak_error(chunk: object) -> bool: def is_server_fulfilled_tool_leak_error(chunk: object) -> bool:
@ -76,10 +80,10 @@ def _parse_sse_events(raw: bytes) -> list[tuple]:
continue continue
data_str = stripped[len("data:") :].strip() data_str = stripped[len("data:") :].strip()
try: try:
data = json.loads(data_str) data = _DECODED_JSON.validate_python(json.loads(data_str))
except (json.JSONDecodeError, ValueError): except (json.JSONDecodeError, ValueError):
continue continue
event_type = current_event_type or data.get("type", "") event_type = current_event_type or decoded_json_object(data).get("type", "")
current_event_type = None current_event_type = None
events.append((event_type, data)) events.append((event_type, data))
return events return events
@ -371,7 +375,7 @@ class AgenticAnthropicStreamingIterator:
verbose_logger.debug("AgenticStreamingIterator: Could not rebuild response from SSE bytes") verbose_logger.debug("AgenticStreamingIterator: Could not rebuild response from SSE bytes")
return return
result: Final = await self._http_handler._call_agentic_completion_hooks( result: Final[object] = await self._http_handler._call_agentic_completion_hooks(
response=rebuilt, response=rebuilt,
model=self._model, model=self._model,
messages=self._messages, messages=self._messages,
@ -386,7 +390,7 @@ class AgenticAnthropicStreamingIterator:
if result is None: if result is None:
return return
if hasattr(result, "__aiter__"): if has_attributes(result, AsyncIterable[object], "__aiter__"):
self._follow_up_iterator = result.__aiter__() self._follow_up_iterator = result.__aiter__()
elif isinstance(result, dict): elif isinstance(result, dict):
from litellm.llms.anthropic.messages.native.fake_stream_iterator import ( from litellm.llms.anthropic.messages.native.fake_stream_iterator import (

View file

@ -2,8 +2,8 @@
## Translates OpenAI call to Anthropic `/v1/messages` format ## Translates OpenAI call to Anthropic `/v1/messages` format
import asyncio import asyncio
from collections import deque from collections import deque
from collections.abc import AsyncGenerator, Awaitable, Callable, Iterator, Mapping, Sequence from collections.abc import AsyncGenerator, AsyncIterable, Awaitable, Callable, Iterable, Iterator, Mapping, Sequence
from typing import TYPE_CHECKING, Any, Final from typing import TYPE_CHECKING, Final
import anyio import anyio
from pydantic import ConfigDict, field_validator from pydantic import ConfigDict, field_validator
@ -12,6 +12,7 @@ from litellm import verbose_logger
from litellm._logging import redact_internal_details_from_client_message from litellm._logging import redact_internal_details_from_client_message
from litellm._uuid import uuid from litellm._uuid import uuid
from litellm.exceptions import APIError, MidStreamFallbackError from litellm.exceptions import APIError, MidStreamFallbackError
from litellm.litellm_core_utils.core_helpers import has_attributes, iter_items
from litellm.litellm_core_utils.prompt_templates.reasoning_utils import ( from litellm.litellm_core_utils.prompt_templates.reasoning_utils import (
encrypted_reasoning_signature, encrypted_reasoning_signature,
) )
@ -126,7 +127,7 @@ class AnthropicResponsesStreamWrapper:
def __init__( def __init__(
self, self,
responses_stream: Any, responses_stream: object,
model: str, model: str,
litellm_logging_obj: "LiteLLMLoggingObject | None" = None, litellm_logging_obj: "LiteLLMLoggingObject | None" = None,
safeguards_evaluator: "SafeguardsEvaluator | None" = None, safeguards_evaluator: "SafeguardsEvaluator | None" = None,
@ -414,7 +415,9 @@ class AnthropicResponsesStreamWrapper:
response_obj: Final = getattr(event, "response", None) or ( response_obj: Final = getattr(event, "response", None) or (
event.get("response") if isinstance(event, dict) else None event.get("response") if isinstance(event, dict) else None
) )
output: Final = (getattr(response_obj, "output", None) or ()) if response_obj is not None else () output: Final[Iterable[object]] = (
(getattr(response_obj, "output", None) or ()) if response_obj is not None else ()
)
refusal_text: Final = responses_output_refusal_text(output) or (self._refusal_text or None) refusal_text: Final = responses_output_refusal_text(output) or (self._refusal_text or None)
status: Final = getattr(response_obj, "status", None) if response_obj is not None else None status: Final = getattr(response_obj, "status", None) if response_obj is not None else None
has_tool_call: Final = any( has_tool_call: Final = any(
@ -508,14 +511,14 @@ class AnthropicResponsesStreamWrapper:
return self._chunk_queue.popleft() return self._chunk_queue.popleft()
try: try:
if hasattr(self.responses_stream, "__aiter__"): if has_attributes(self.responses_stream, AsyncIterable[object], "__aiter__"):
async for event in self.responses_stream: async for event in self.responses_stream:
self._process_event(event) self._process_event(event)
if self._chunk_queue: if self._chunk_queue:
return self._chunk_queue.popleft() return self._chunk_queue.popleft()
else: else:
if self._sync_responses_iterator is None: if self._sync_responses_iterator is None:
self._sync_responses_iterator = iter(self.responses_stream) self._sync_responses_iterator = iter(iter_items(self.responses_stream))
sync_iterator: Final = self._sync_responses_iterator sync_iterator: Final = self._sync_responses_iterator
missing: Final = object() missing: Final = object()
while (event := await asyncio.to_thread(next, sync_iterator, missing)) is not missing: while (event := await asyncio.to_thread(next, sync_iterator, missing)) is not missing:

View file

@ -10,6 +10,8 @@ from collections.abc import Iterable, Mapping, Sequence
from itertools import groupby from itertools import groupby
from typing import Any, Final, cast from typing import Any, Final, cast
from pydantic import TypeAdapter
from litellm.litellm_core_utils.prompt_templates.common_utils import ( from litellm.litellm_core_utils.prompt_templates.common_utils import (
TOOL_RESULT_IMAGE_BOUNDARY, TOOL_RESULT_IMAGE_BOUNDARY,
TOOL_RESULT_IMAGE_PLACEHOLDER, TOOL_RESULT_IMAGE_PLACEHOLDER,
@ -58,6 +60,7 @@ from litellm.types.llms.openai import (
REASONING_SUMMARY_PART_SEPARATOR: Final = "\n\n" REASONING_SUMMARY_PART_SEPARATOR: Final = "\n\n"
RESPONSES_INCLUDE_ENCRYPTED_REASONING: Final = "reasoning.encrypted_content" RESPONSES_INCLUDE_ENCRYPTED_REASONING: Final = "reasoning.encrypted_content"
_DECODED_JSON: Final = TypeAdapter(object)
class LiteLLMAnthropicToResponsesAPIAdapter: class LiteLLMAnthropicToResponsesAPIAdapter:
@ -805,15 +808,17 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
content.append(reasoning_block) content.append(reasoning_block)
elif item_type == "function_call": elif item_type == "function_call":
try: try:
input_data = json.loads(item.get("arguments", "{}")) input_data = _DECODED_JSON.validate_python(json.loads(item.get("arguments", "{}")))
except (json.JSONDecodeError, TypeError): except (json.JSONDecodeError, TypeError):
input_data = {} input_data = {}
content.append( content.append(
AnthropicResponseContentBlockToolUse( AnthropicResponseContentBlockToolUse.model_validate(
type="tool_use", {
id=item.get("call_id") or item.get("id", ""), "type": "tool_use",
name=item.get("name", ""), "id": item.get("call_id") or item.get("id", ""),
input=input_data, "name": item.get("name", ""),
"input": input_data,
}
).model_dump(exclude_none=True) ).model_dump(exclude_none=True)
) )
stop_reason = "tool_use" stop_reason = "tool_use"

View file

@ -4,10 +4,13 @@ from collections.abc import Mapping, Sequence
from types import MappingProxyType from types import MappingProxyType
from typing import Final from typing import Final
from pydantic import TypeAdapter
import litellm import litellm
from litellm._logging import verbose_proxy_logger from litellm._logging import verbose_proxy_logger
from litellm.litellm_core_utils.core_helpers import as_str_mapping, map_finish_reason from litellm.litellm_core_utils.core_helpers import as_str_mapping, decoded_json_object, map_finish_reason
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.litellm_core_utils.llm_cost_calc.guardrail_cost import is_str_keyed_dict
from litellm.llms.anthropic.chat.handler import ( from litellm.llms.anthropic.chat.handler import (
ModelResponseIterator as AnthropicModelResponseIterator, ModelResponseIterator as AnthropicModelResponseIterator,
) )
@ -21,6 +24,8 @@ from litellm.types.utils import (
TextCompletionResponse, TextCompletionResponse,
) )
_DECODED_JSON: Final = TypeAdapter(object)
def split_sse_chunk_into_events(chunk: str | bytes) -> list[str]: def split_sse_chunk_into_events(chunk: str | bytes) -> list[str]:
""" """
@ -98,6 +103,14 @@ _FAST_PATH_DISALLOWED_DELTA_TYPES: Final = frozenset(
) )
def _joined_text(parts: Sequence[object]) -> str:
strings: Final = [part for part in parts if isinstance(part, str)]
if len(strings) == len(parts):
return "".join(strings)
first_bad: Final = next(position for position, part in enumerate(parts) if not isinstance(part, str))
raise TypeError(f"sequence item {first_bad}: expected str instance, {type(parts[first_bad]).__name__} found")
def collapse_pure_text_chunks( def collapse_pure_text_chunks(
all_chunks: Sequence[str | bytes], all_chunks: Sequence[str | bytes],
) -> list[str] | None: ) -> list[str] | None:
@ -120,10 +133,10 @@ def collapse_pure_text_chunks(
if ev: if ev:
normalized.append(ev) normalized.append(ev)
text_block_indexes: Final[set] = set() text_block_indexes: Final[set[object]] = set()
out: Final[list[str]] = [] out: Final[list[str]] = []
pending_text: list[str] = [] pending_text: list[object] = []
pending_index: int | None = None pending_index: object = None
saw_any_text_delta = False saw_any_text_delta = False
def flush() -> None: def flush() -> None:
@ -132,7 +145,7 @@ def collapse_pure_text_chunks(
merged: Final = { merged: Final = {
"type": "content_block_delta", "type": "content_block_delta",
"index": pending_index if pending_index is not None else 0, "index": pending_index if pending_index is not None else 0,
"delta": {"type": "text_delta", "text": "".join(pending_text)}, "delta": {"type": "text_delta", "text": _joined_text(pending_text)},
} }
out.append("data: " + json.dumps(merged)) out.append("data: " + json.dumps(merged))
pending_text = [] pending_text = []
@ -158,20 +171,20 @@ def collapse_pure_text_chunks(
if not json_str.startswith("{"): if not json_str.startswith("{"):
continue continue
try: try:
data = json.loads(json_str) data = decoded_json_object(_DECODED_JSON.validate_python(json.loads(json_str)))
except (json.JSONDecodeError, ValueError): except (json.JSONDecodeError, ValueError):
return None return None
etype = data.get("type") etype = data.get("type")
if etype == "content_block_start": if etype == "content_block_start":
block = data.get("content_block") or {} block = decoded_json_object(data.get("content_block") or {})
if block.get("type") != "text": if block.get("type") != "text":
return None return None
text_block_indexes.add(data.get("index")) text_block_indexes.add(data.get("index"))
flush() flush()
out.append(ev) out.append(ev)
elif etype == "content_block_delta": elif etype == "content_block_delta":
delta = data.get("delta") or {} delta = decoded_json_object(data.get("delta") or {})
dtype = delta.get("type") dtype = delta.get("type")
if dtype in _FAST_PATH_DISALLOWED_DELTA_TYPES: if dtype in _FAST_PATH_DISALLOWED_DELTA_TYPES:
return None return None
@ -291,9 +304,10 @@ def extract_sse_data(event_str: str) -> dict | None:
if payload is None or not payload.startswith("{"): if payload is None or not payload.startswith("{"):
return None return None
try: try:
return json.loads(payload) decoded: Final = _DECODED_JSON.validate_python(json.loads(payload))
except (ValueError, TypeError): except (ValueError, TypeError):
return None return None
return decoded if is_str_keyed_dict(decoded) else None
def build_usage_only_response_from_chunks( def build_usage_only_response_from_chunks(
@ -492,7 +506,7 @@ def _carries_no_data(fields: Sequence[tuple[str, str]]) -> bool:
def _json_mapping(payload: str) -> Mapping[str, object] | None: def _json_mapping(payload: str) -> Mapping[str, object] | None:
try: try:
value: Final[object] = json.loads(payload) value: Final = _DECODED_JSON.validate_python(json.loads(payload))
except ValueError: except ValueError:
return None return None
return as_str_mapping(value) return as_str_mapping(value)
@ -555,7 +569,7 @@ def is_anthropic_sse_stream(all_chunks: Sequence[object]) -> bool:
def _is_json_no_reader_can_take(payload: str) -> bool: def _is_json_no_reader_can_take(payload: str) -> bool:
try: try:
value: Final[object] = json.loads(payload) value: Final = _DECODED_JSON.validate_python(json.loads(payload))
except ValueError: except ValueError:
return False return False
if value is None: if value is None:

View file

@ -12,7 +12,9 @@ from typing import Final, Literal, cast
from urllib.parse import urlencode from urllib.parse import urlencode
import httpx import httpx
from pydantic import TypeAdapter
from litellm.litellm_core_utils.core_helpers import as_str_mapping, decoded_json_object, iter_items
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.litellm_core_utils.url_utils import ensure_api_route from litellm.litellm_core_utils.url_utils import ensure_api_route
from litellm.llms.apiserpent.search.defaults import ( from litellm.llms.apiserpent.search.defaults import (
@ -33,6 +35,7 @@ from litellm.secret_managers.main import get_secret_str
DEEP_SEARCH_PARAM: Final = "deep" DEEP_SEARCH_PARAM: Final = "deep"
APISERPENT_BASE: Final = "https://apiserpent.com" APISERPENT_BASE: Final = "https://apiserpent.com"
APISERPENT_PARAMS_KEY: Final = "_apiserpent_params" APISERPENT_PARAMS_KEY: Final = "_apiserpent_params"
_DECODED_JSON: Final = TypeAdapter(object)
class APISerpentSearchConfig(BaseSearchConfig): class APISerpentSearchConfig(BaseSearchConfig):
@ -150,22 +153,24 @@ class APISerpentSearchConfig(BaseSearchConfig):
Full format nests results under ``results.organic[]``; simple format Full format nests results under ``results.organic[]``; simple format
returns a flat ``results[]`` array. Both expose title/url/snippet. returns a flat ``results[]`` array. Both expose title/url/snippet.
""" """
response_json: Final = raw_response.json() response_json: Final = decoded_json_object(_DECODED_JSON.validate_python(raw_response.json()))
raw_results: Final = response_json.get("results") or {} raw_results: Final = response_json.get("results") or {}
organic: Final = raw_results.get("organic", []) if isinstance(raw_results, dict) else raw_results nested_results: Final = as_str_mapping(raw_results)
organic: Final = nested_results.get("organic", []) if nested_results is not None else raw_results
results: Final[list[SearchResult]] = [] results: Final = [
for result in organic: SearchResult.model_validate(
results.append( {
SearchResult( "title": result.get("title", ""),
title=result.get("title", ""), "url": result.get("url", ""),
url=result.get("url", ""), "snippet": result.get("snippet", ""),
snippet=result.get("snippet", ""), "date": result.get("date"),
date=result.get("date"), "last_updated": None,
last_updated=None, }
)
) )
for result in map(decoded_json_object, iter_items(organic))
]
return SearchResponse( return SearchResponse(
results=results, results=results,

View file

@ -2,7 +2,7 @@ import asyncio
import json import json
import time import time
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
from typing import Final from typing import Final, Protocol
import httpx import httpx
from openai import ( from openai import (
@ -17,6 +17,7 @@ from pydantic import BaseModel
import litellm import litellm
from litellm.constants import DEFAULT_MAX_RETRIES from litellm.constants import DEFAULT_MAX_RETRIES
from litellm.litellm_core_utils.core_helpers import require_attributes
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.litellm_core_utils.logging_utils import speech_request_body, track_llm_api_timing from litellm.litellm_core_utils.logging_utils import speech_request_body, track_llm_api_timing
from litellm.llms.custom_httpx.http_handler import ( from litellm.llms.custom_httpx.http_handler import (
@ -60,6 +61,11 @@ from .image_generation import (
from .image_generation.http_utils import azure_deployment_image_generation_json_body from .image_generation.http_utils import azure_deployment_image_generation_json_body
class _HasResponseText(Protocol):
@property
def text(self) -> str: ...
class AzureOpenAIAssistantsAPIConfig: class AzureOpenAIAssistantsAPIConfig:
""" """
Reference: https://learn.microsoft.com/en-us/azure/ai-services/openai/assistants-reference-messages?tabs=python#create-message Reference: https://learn.microsoft.com/en-us/azure/ai-services/openai/assistants-reference-messages?tabs=python#create-message
@ -853,11 +859,11 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM):
except Exception as e: except Exception as e:
status_code: Final = getattr(e, "status_code", 500) status_code: Final = getattr(e, "status_code", 500)
error_headers = getattr(e, "headers", None) error_headers = getattr(e, "headers", None)
error_response: Final = getattr(e, "response", None) error_response: Final[object] = getattr(e, "response", None)
error_text = str(e) error_text = str(e)
if error_headers is None and error_response: if error_headers is None and error_response:
error_headers = getattr(error_response, "headers", None) error_headers = getattr(error_response, "headers", None)
error_text = error_response.text error_text = require_attributes(error_response, _HasResponseText, "text").text
raise AzureOpenAIError( raise AzureOpenAIError(
status_code=status_code, status_code=status_code,
message=error_text, message=error_text,

View file

@ -251,9 +251,8 @@ class AzureOpenAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
def compact_url(self, api_base: str) -> str: def compact_url(self, api_base: str) -> str:
params: Final = httpx.URL(api_base).params params: Final = httpx.URL(api_base).params
v1_params: Final = ( api_version: Final = next(iter(params.get_list("api-version")), None)
params if BaseAzureLLM.is_azure_v1_api_version(params.get("api-version")) else params.remove("api-version") v1_params: Final = params if BaseAzureLLM.is_azure_v1_api_version(api_version) else params.remove("api-version")
)
return super().compact_url(str(_v1_responses_url(api_base).copy_with(params=v1_params))) return super().compact_url(str(_v1_responses_url(api_base).copy_with(params=v1_params)))
######################################################### #########################################################

View file

@ -308,7 +308,7 @@ class AzureAIAgentsHandler:
logging_obj: LiteLLMLoggingObj, logging_obj: LiteLLMLoggingObj,
optional_params: dict, optional_params: dict,
litellm_params: dict, litellm_params: dict,
timeout: float, timeout: float | str | httpx.Timeout | None,
client: HTTPHandler | None = None, client: HTTPHandler | None = None,
headers: dict | None = None, headers: dict | None = None,
) -> ModelResponse: ) -> ModelResponse:
@ -432,7 +432,7 @@ class AzureAIAgentsHandler:
logging_obj: LiteLLMLoggingObj, logging_obj: LiteLLMLoggingObj,
optional_params: dict, optional_params: dict,
litellm_params: dict, litellm_params: dict,
timeout: float, timeout: float | str | httpx.Timeout | None,
client: AsyncHTTPHandler | None = None, client: AsyncHTTPHandler | None = None,
headers: dict | None = None, headers: dict | None = None,
) -> ModelResponse: ) -> ModelResponse:
@ -559,7 +559,7 @@ class AzureAIAgentsHandler:
logging_obj: LiteLLMLoggingObj, logging_obj: LiteLLMLoggingObj,
optional_params: dict, optional_params: dict,
litellm_params: dict, litellm_params: dict,
timeout: float, timeout: float | str | httpx.Timeout | None,
headers: dict | None = None, headers: dict | None = None,
) -> AsyncIterator[ModelResponseStream]: ) -> AsyncIterator[ModelResponseStream]:
"""Execute async streaming completion using Azure Agent Service with native SSE.""" """Execute async streaming completion using Azure Agent Service with native SSE."""

View file

@ -323,7 +323,7 @@ class AzureAIAgentsConfig(BaseConfig):
logging_obj: LiteLLMLoggingObj, logging_obj: LiteLLMLoggingObj,
optional_params: dict, optional_params: dict,
litellm_params: dict, litellm_params: dict,
timeout: float | Any, timeout: float | str | httpx.Timeout | None,
acompletion: bool, acompletion: bool,
stream: bool | None = False, stream: bool | None = False,
headers: dict | None = None, headers: dict | None = None,

View file

@ -8,7 +8,7 @@ from collections.abc import Mapping
from typing import Final from typing import Final
import httpx import httpx
from pydantic import JsonValue from pydantic import JsonValue, TypeAdapter
import litellm import litellm
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
@ -22,6 +22,8 @@ from litellm.llms.azure_ai.anthropic.count_tokens.transformation import (
) )
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
_DECODED_JSON: Final = TypeAdapter(object)
class AzureAIAnthropicCountTokensHandler(AzureAIAnthropicCountTokensConfig): class AzureAIAnthropicCountTokensHandler(AzureAIAnthropicCountTokensConfig):
""" """
@ -42,7 +44,7 @@ class AzureAIAnthropicCountTokensHandler(AzureAIAnthropicCountTokensConfig):
system: object = None, system: object = None,
optional_params: Mapping[str, JsonValue] | None = None, optional_params: Mapping[str, JsonValue] | None = None,
context_management: JsonValue = None, context_management: JsonValue = None,
) -> dict[str, object]: ) -> object:
""" """
Handle a CountTokens request using httpx with Azure authentication. Handle a CountTokens request using httpx with Azure authentication.
@ -126,7 +128,7 @@ class AzureAIAnthropicCountTokensHandler(AzureAIAnthropicCountTokensConfig):
message=error_text, message=error_text,
) )
azure_response: Final = response.json() azure_response: Final = _DECODED_JSON.validate_python(response.json())
verbose_logger.debug("Azure AI Anthropic response: %s", azure_response) verbose_logger.debug("Azure AI Anthropic response: %s", azure_response)

View file

@ -11,6 +11,7 @@ from pydantic import JsonValue
import litellm import litellm
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
from litellm.litellm_core_utils.core_helpers import decoded_json_object
from litellm.llms.anthropic.count_tokens.transformation import ( from litellm.llms.anthropic.count_tokens.transformation import (
AnthropicCountFields, AnthropicCountFields,
ChatCountTranslationError, ChatCountTranslationError,
@ -139,13 +140,14 @@ class AzureAIAnthropicTokenCounter(BaseTokenCounter):
) )
if result is not None: if result is not None:
response_fields: Final = decoded_json_object(result)
return TokenCountResponse.model_validate( return TokenCountResponse.model_validate(
{ {
"total_tokens": result.get("input_tokens", 0), "total_tokens": response_fields.get("input_tokens", 0),
"request_model": request_model, "request_model": request_model,
"model_used": model_to_use, "model_used": model_to_use,
"tokenizer_type": "azure_ai_anthropic_api", "tokenizer_type": "azure_ai_anthropic_api",
"original_response": result, "original_response": response_fields,
} }
) )
except AnthropicError as e: except AnthropicError as e:

View file

@ -3,7 +3,7 @@ Azure Anthropic messages transformation config - extends AnthropicMessagesConfig
""" """
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any, Final from typing import Final
from litellm.llms.anthropic.messages.native.transformation import ( from litellm.llms.anthropic.messages.native.transformation import (
AnthropicMessagesConfig, AnthropicMessagesConfig,
@ -109,14 +109,10 @@ class AzureAnthropicMessagesConfig(AnthropicMessagesConfig):
Processes both `system` and `messages` content blocks. Processes both `system` and `messages` content blocks.
""" """
def _sanitize(cache_control: Any) -> None:
if isinstance(cache_control, dict):
cache_control.pop("scope", None)
def _process_content_list(content: list) -> None: def _process_content_list(content: list) -> None:
for item in content: for item in content:
if isinstance(item, dict) and "cache_control" in item: if isinstance(item, dict) and "cache_control" in item and isinstance(item["cache_control"], dict):
_sanitize(item["cache_control"]) item["cache_control"].pop("scope", None)
if "system" in anthropic_messages_request: if "system" in anthropic_messages_request:
system: Final = anthropic_messages_request["system"] system: Final = anthropic_messages_request["system"]

View file

@ -2,11 +2,12 @@ import base64
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from io import BufferedReader from io import BufferedReader
from types import MappingProxyType from types import MappingProxyType
from typing import Any, Final from typing import Final, Protocol
from httpx._types import RequestFiles from httpx._types import RequestFiles
import litellm import litellm
from litellm.litellm_core_utils.core_helpers import has_attributes
from litellm.llms.azure_ai.common_utils import ( from litellm.llms.azure_ai.common_utils import (
AzureFoundryModelInfo, AzureFoundryModelInfo,
get_azure_ai_auth_headers, get_azure_ai_auth_headers,
@ -21,6 +22,10 @@ from litellm.types.llms.openai import FileTypes
from litellm.types.router import GenericLiteLLMParams from litellm.types.router import GenericLiteLLMParams
class _BinaryReadable(Protocol):
def read(self) -> bytes: ...
class AzureFoundryFlux2ImageEditConfig(OpenAIImageEditConfig): class AzureFoundryFlux2ImageEditConfig(OpenAIImageEditConfig):
""" """
Azure AI Foundry FLUX 2 image edit config Azure AI Foundry FLUX 2 image edit config
@ -100,7 +105,7 @@ class AzureFoundryFlux2ImageEditConfig(OpenAIImageEditConfig):
if image is None: if image is None:
raise ValueError("FLUX 2 image edit requires an image.") raise ValueError("FLUX 2 image edit requires an image.")
images: Final = tuple(image) if isinstance(image, list) else (image,) images: Final[Sequence[object]] = [*image] if isinstance(image, list) else [image]
if not images: if not images:
raise ValueError("FLUX 2 image edit requires at least one image.") raise ValueError("FLUX 2 image edit requires at least one image.")
max_reference_images: Final = 10 if "flex" in model.lower() else 8 max_reference_images: Final = 10 if "flex" in model.lower() else 8
@ -121,14 +126,14 @@ class AzureFoundryFlux2ImageEditConfig(OpenAIImageEditConfig):
} }
return request_body, [] return request_body, []
def _convert_image_to_base64(self, image: Any) -> str: def _convert_image_to_base64(self, image: object) -> str:
"""Convert image file to base64 string""" """Convert image file to base64 string"""
if isinstance(image, BufferedReader): if isinstance(image, BufferedReader):
image_bytes = image.read() image_bytes = image.read()
image.seek(0) # Reset file pointer for potential reuse image.seek(0) # Reset file pointer for potential reuse
elif isinstance(image, bytes): elif isinstance(image, bytes):
image_bytes = image image_bytes = image
elif hasattr(image, "read"): elif has_attributes(image, _BinaryReadable, "read"):
image_bytes = image.read() image_bytes = image.read()
else: else:
raise ValueError(f"Unsupported image type: {type(image)}") raise ValueError(f"Unsupported image type: {type(image)}")

View file

@ -1,7 +1,9 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Sequence from collections.abc import Sequence
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, ClassVar, Final, Optional from typing import TYPE_CHECKING, Any, ClassVar, Final, Optional, Protocol
from litellm.litellm_core_utils.core_helpers import has_attributes
if TYPE_CHECKING: if TYPE_CHECKING:
from fastapi import HTTPException from fastapi import HTTPException
@ -15,6 +17,10 @@ if TYPE_CHECKING:
from litellm.types.proxy.auth.user_api_key_auth import UserAPIKeyAuth from litellm.types.proxy.auth.user_api_key_auth import UserAPIKeyAuth
class _ModelDumpable(Protocol):
def model_dump(self) -> object: ...
@dataclass(slots=True) @dataclass(slots=True)
class StreamTransformSink: class StreamTransformSink:
"""Out-parameter used by ``process_output_streaming_response`` to hand the """Out-parameter used by ``process_output_streaming_response`` to hand the
@ -81,7 +87,7 @@ class BaseTranslation(ABC):
@staticmethod @staticmethod
def transform_user_api_key_dict_to_metadata( def transform_user_api_key_dict_to_metadata(
user_api_key_dict: Any | None, user_api_key_dict: object,
) -> dict[str, object]: ) -> dict[str, object]:
""" """
Transform user_api_key_dict to a metadata dict with prefixed keys. Transform user_api_key_dict to a metadata dict with prefixed keys.
@ -99,7 +105,11 @@ class BaseTranslation(ABC):
return {} return {}
# Convert to dict if it's a Pydantic object # Convert to dict if it's a Pydantic object
user_dict = user_api_key_dict.model_dump() if hasattr(user_api_key_dict, "model_dump") else user_api_key_dict user_dict: Final = (
user_api_key_dict.model_dump()
if has_attributes(user_api_key_dict, _ModelDumpable, "model_dump")
else user_api_key_dict
)
if not isinstance(user_dict, dict): if not isinstance(user_dict, dict):
return {} return {}

View file

@ -11,8 +11,9 @@ are denied so an empty user_id can never select an unscoped query.
""" """
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from typing import Any, Final from typing import Final, Protocol
from litellm.litellm_core_utils.core_helpers import require_attributes
from litellm.types.proxy.auth.user_api_key_auth import ( from litellm.types.proxy.auth.user_api_key_auth import (
UserAPIKeyAuth, UserAPIKeyAuth,
) )
@ -47,15 +48,20 @@ def resolve_resource_owner_id(
return None return None
def build_list_page(items: list[Any], has_more: bool = False) -> dict[str, object]: class _Identified(Protocol):
@property
def id(self) -> object: ...
def build_list_page(items: Sequence[object], has_more: bool = False) -> dict[str, object]:
"""Build the OpenAI-style paginated list response shape used by managed """Build the OpenAI-style paginated list response shape used by managed
file/batch/vector-store listings. ``first_id`` and ``last_id`` are file/batch/vector-store listings. ``first_id`` and ``last_id`` are
sourced from each item's ``.id`` attribute.""" sourced from each item's ``.id`` attribute."""
return { return {
"object": "list", "object": "list",
"data": items, "data": items,
"first_id": items[0].id if items else None, "first_id": require_attributes(items[0], _Identified, "id").id if items else None,
"last_id": items[-1].id if items else None, "last_id": require_attributes(items[-1], _Identified, "id").id if items else None,
"has_more": has_more, "has_more": has_more,
} }

View file

@ -11,7 +11,7 @@ from litellm import verbose_logger
from litellm._uuid import uuid from litellm._uuid import uuid
from litellm.caching.caching import InMemoryCache from litellm.caching.caching import InMemoryCache
from litellm.constants import RESPONSE_FORMAT_TOOL_NAME from litellm.constants import RESPONSE_FORMAT_TOOL_NAME
from litellm.litellm_core_utils.core_helpers import map_finish_reason from litellm.litellm_core_utils.core_helpers import decoded_json_object, map_finish_reason
from litellm.litellm_core_utils.litellm_logging import Logging from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.litellm_core_utils.llm_cost_calc.guardrail_cost import object_mapping from litellm.litellm_core_utils.llm_cost_calc.guardrail_cost import object_mapping
from litellm.litellm_core_utils.llm_response_utils.get_headers import provider_request_id_headers from litellm.litellm_core_utils.llm_response_utils.get_headers import provider_request_id_headers
@ -50,6 +50,7 @@ from litellm.types.utils import GenericStreamingChunk as GChunk
from ..common_utils import ( from ..common_utils import (
BedrockError, BedrockError,
BedrockEventStreamResponseDict, BedrockEventStreamResponseDict,
bedrock_event_chunk_text,
bedrock_event_stream_header, bedrock_event_stream_header,
bedrock_event_stream_response, bedrock_event_stream_response,
bedrock_stream_event_error_status, bedrock_stream_event_error_status,
@ -59,6 +60,7 @@ from ..common_utils import (
get_bedrock_response_stream_shape, get_bedrock_response_stream_shape,
get_bedrock_stream_event_statuses, get_bedrock_stream_event_statuses,
get_bedrock_tool_name, get_bedrock_tool_name,
parse_bedrock_event_stream,
) )
bedrock_tool_name_mappings: Final[InMemoryCache] = InMemoryCache(max_size_in_memory=50, default_ttl=600) bedrock_tool_name_mappings: Final[InMemoryCache] = InMemoryCache(max_size_in_memory=50, default_ttl=600)
@ -922,7 +924,7 @@ class AWSEventStreamDecoder:
response_stream_shape: "Shape", response_stream_shape: "Shape",
request_id_headers: httpx.Headers | None = None, request_id_headers: httpx.Headers | None = None,
) -> str | None: ) -> str | None:
parsed_response: Final = self.parser.parse(response_dict, response_stream_shape) parsed_response: Final = parse_bedrock_event_stream(self.parser, response_dict, response_stream_shape)
if response_dict["status_code"] != 200: if response_dict["status_code"] != 200:
raise build_bedrock_stream_error(response_dict, response_stream_shape, headers=request_id_headers) raise build_bedrock_stream_error(response_dict, response_stream_shape, headers=request_id_headers)
@ -932,11 +934,12 @@ class AWSEventStreamDecoder:
raise build_bedrock_stream_event_error( raise build_bedrock_stream_event_error(
event_type, event_error_status, response_dict["body"], headers=request_id_headers event_type, event_error_status, response_dict["body"], headers=request_id_headers
) )
if "chunk" in parsed_response: parsed_fields: Final = decoded_json_object(parsed_response)
chunk = parsed_response.get("chunk") if "chunk" in parsed_fields:
if not chunk: event_chunk: Final = parsed_fields.get("chunk")
if not event_chunk:
return None return None
return chunk.get("bytes").decode() return bedrock_event_chunk_text(event_chunk)
else: else:
chunk = response_dict.get("body") chunk = response_dict.get("body")
if not chunk: if not chunk:

View file

@ -13,7 +13,7 @@ import warnings
from collections.abc import Callable, Iterator, Mapping, Sequence from collections.abc import Callable, Iterator, Mapping, Sequence
from dataclasses import dataclass from dataclasses import dataclass
from types import MappingProxyType from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Final, Literal, TypeAlias from typing import TYPE_CHECKING, Final, Literal, Protocol, TypeAlias
from typing_extensions import ( from typing_extensions import (
ReadOnly, ReadOnly,
@ -24,6 +24,7 @@ from typing_extensions import (
if TYPE_CHECKING: if TYPE_CHECKING:
from botocore.eventstream import EventStreamMessage from botocore.eventstream import EventStreamMessage
from botocore.model import ServiceModel, Shape from botocore.model import ServiceModel, Shape
from botocore.parsers import EventStreamJSONParser
from litellm.types.llms.bedrock import BedrockCreateBatchRequest from litellm.types.llms.bedrock import BedrockCreateBatchRequest
@ -33,7 +34,7 @@ from pydantic import ConfigDict, TypeAdapter, ValidationError
import litellm import litellm
from litellm import verbose_logger from litellm import verbose_logger
from litellm.litellm_core_utils.aws_partition import get_aws_dns_suffix from litellm.litellm_core_utils.aws_partition import get_aws_dns_suffix
from litellm.litellm_core_utils.core_helpers import lax_bool from litellm.litellm_core_utils.core_helpers import decoded_json_object, lax_bool, require_attributes
from litellm.litellm_core_utils.optional_imports import ensure_optional_import from litellm.litellm_core_utils.optional_imports import ensure_optional_import
from litellm.litellm_core_utils.prompt_templates.json_schema_utils import ( from litellm.litellm_core_utils.prompt_templates.json_schema_utils import (
schema_property_count, schema_property_count,
@ -135,7 +136,7 @@ _BEDROCK_AWS_AUTH_PARAMETER_KEYS: Final[tuple[str, ...]] = (*AWS_AUTH_PARAM_KEYS
def merge_bedrock_aws_request_params( def merge_bedrock_aws_request_params(
litellm_params: Mapping[str, object], litellm_params: Mapping[str, object],
optional_params: Mapping[str, object], optional_params: Mapping[str, object],
) -> dict[str, Any]: ) -> dict[str, object]:
"""Merge deployment and request parameters without allowing auth escalation. """Merge deployment and request parameters without allowing auth escalation.
Deployment configuration is authoritative for AWS authentication. When a Deployment configuration is authoritative for AWS authentication. When a
@ -2516,15 +2517,16 @@ class BedrockEventStreamDecoderBase:
), ),
) )
response_dict: Final = event.to_response_dict() response_dict: Final = event.to_response_dict()
parsed_response: Final = self.parser.parse(response_dict, response_stream_shape) parsed_response: Final = parse_bedrock_event_stream(self.parser, response_dict, response_stream_shape)
if response_dict["status_code"] != 200: if response_dict["status_code"] != 200:
raise build_bedrock_stream_error(response_dict, response_stream_shape) raise build_bedrock_stream_error(response_dict, response_stream_shape)
if "chunk" in parsed_response: parsed_fields: Final = decoded_json_object(parsed_response)
chunk = parsed_response.get("chunk") if "chunk" in parsed_fields:
if not chunk: event_chunk: Final = parsed_fields.get("chunk")
if not event_chunk:
return None return None
return chunk.get("bytes").decode() return bedrock_event_chunk_text(event_chunk)
else: else:
chunk = response_dict.get("body") chunk = response_dict.get("body")
if not chunk: if not chunk:
@ -2541,6 +2543,20 @@ def _decoded_json_value(raw: str) -> object:
return _JSON_VALUE.validate_python(json.loads(raw)) return _JSON_VALUE.validate_python(json.loads(raw))
class _DecodablePayload(Protocol):
def decode(self) -> str: ...
def parse_bedrock_event_stream(
parser: EventStreamJSONParser, response_dict: Mapping[str, object], response_stream_shape: Shape
) -> object:
return _JSON_VALUE.validate_python(parser.parse(response_dict, response_stream_shape))
def bedrock_event_chunk_text(chunk: object) -> str:
return require_attributes(decoded_json_object(chunk).get("bytes"), _DecodablePayload, "decode").decode()
def get_anthropic_beta_from_headers(headers: dict) -> list[str]: def get_anthropic_beta_from_headers(headers: dict) -> list[str]:
""" """
Extract anthropic-beta header values and convert them to a list. Extract anthropic-beta header values and convert them to a list.

View file

@ -45,7 +45,7 @@ class _CountTokensReply(LiteLLMPydanticObjectBase):
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class CountedTokens: class CountedTokens:
input_tokens: int input_tokens: int
original_response: Mapping[str, JsonValue] original_response: Mapping[str, object]
tokenizer_type: str tokenizer_type: str

View file

@ -7,14 +7,18 @@ Simplified handler leveraging existing LiteLLM Bedrock infrastructure.
from typing import Any, Final from typing import Any, Final
import httpx import httpx
from pydantic import TypeAdapter
import litellm import litellm
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
from litellm.litellm_core_utils.core_helpers import decoded_json_object
from litellm.llms.bedrock.base_aws_llm import run_aws_signing from litellm.llms.bedrock.base_aws_llm import run_aws_signing
from litellm.llms.bedrock.common_utils import BedrockError from litellm.llms.bedrock.common_utils import BedrockError
from litellm.llms.bedrock.count_tokens.transformation import BedrockCountTokensConfig from litellm.llms.bedrock.count_tokens.transformation import BedrockCountTokensConfig
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, get_async_httpx_client from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, get_async_httpx_client
_DECODED_JSON: Final = TypeAdapter(object)
class BedrockCountTokensHandler(BedrockCountTokensConfig): class BedrockCountTokensHandler(BedrockCountTokensConfig):
""" """
@ -29,7 +33,7 @@ class BedrockCountTokensHandler(BedrockCountTokensConfig):
litellm_params: dict[str, Any], litellm_params: dict[str, Any],
resolved_model: str, resolved_model: str,
client: AsyncHTTPHandler | None = None, client: AsyncHTTPHandler | None = None,
) -> dict[str, Any]: ) -> dict[str, object]:
""" """
Handle a CountTokens request using existing LiteLLM patterns. Handle a CountTokens request using existing LiteLLM patterns.
@ -111,12 +115,12 @@ class BedrockCountTokensHandler(BedrockCountTokensConfig):
response=response, response=response,
) )
bedrock_response: Final = response.json() bedrock_response: Final = _DECODED_JSON.validate_python(response.json())
verbose_logger.debug("Bedrock response: %s", bedrock_response) verbose_logger.debug("Bedrock response: %s", bedrock_response)
# Transform response back to expected format # Transform response back to expected format
final_response: Final = self.transform_bedrock_response_to_anthropic(bedrock_response) final_response: Final = self.transform_bedrock_response_to_anthropic(decoded_json_object(bedrock_response))
verbose_logger.debug("Final response: %s", final_response) verbose_logger.debug("Final response: %s", final_response)

View file

@ -247,9 +247,7 @@ class BedrockCountTokensConfig(BaseAWSLLM):
) )
return join_api_base(base_url, f"model/{encoded_model_id}/count-tokens") return join_api_base(base_url, f"model/{encoded_model_id}/count-tokens")
def transform_bedrock_response_to_anthropic( def transform_bedrock_response_to_anthropic(self, bedrock_response: Mapping[str, object]) -> dict[str, object]:
self, bedrock_response: Mapping[str, JsonValue]
) -> dict[str, JsonValue]:
""" """
Transform Bedrock CountTokens response to Anthropic format. Transform Bedrock CountTokens response to Anthropic format.

View file

@ -11,7 +11,7 @@ from datetime import datetime
from email.utils import parsedate_to_datetime from email.utils import parsedate_to_datetime
from itertools import chain from itertools import chain
from types import MappingProxyType from types import MappingProxyType
from typing import Any, Final, Literal, TypeAlias, TypedDict from typing import Any, Final, Literal, Protocol, TypeAlias, TypedDict
from urllib.parse import quote, unquote, urlencode from urllib.parse import quote, unquote, urlencode
import httpx import httpx
@ -19,6 +19,7 @@ from httpx import Headers, Response
from openai.types.file_deleted import FileDeleted from openai.types.file_deleted import FileDeleted
from pydantic import ConfigDict, Field, TypeAdapter from pydantic import ConfigDict, Field, TypeAdapter
from typing_extensions import ReadOnly from typing_extensions import ReadOnly
from typing_extensions import TypedDict as ExtraItemsTypedDict
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
from litellm._uuid import uuid from litellm._uuid import uuid
@ -38,6 +39,7 @@ from litellm.litellm_core_utils.cloud_storage_security import (
split_configured_cloud_bucket_name, split_configured_cloud_bucket_name,
validate_managed_cloud_file_id, validate_managed_cloud_file_id,
) )
from litellm.litellm_core_utils.core_helpers import require_attributes
from litellm.litellm_core_utils.prompt_templates.common_utils import ( from litellm.litellm_core_utils.prompt_templates.common_utils import (
text_completion_prompt_to_messages, text_completion_prompt_to_messages,
) )
@ -151,6 +153,10 @@ def _retrieved_s3_file_size(raw_response: Response) -> int:
) )
class _SupportsRstrip(Protocol):
def rstrip(self, chars: str, /) -> str: ...
class _S3DeleteContext(LiteLLMBaseModel): class _S3DeleteContext(LiteLLMBaseModel):
file_id: str = Field(min_length=1) file_id: str = Field(min_length=1)
@ -168,6 +174,10 @@ _LITELLM_PARAMS_THE_MAPPER_TAKES: Final = frozenset({"allowed_openai_params"})
_MAPPED_PARAMS_THE_REQUEST_HANDLER_STRIPS: Final = frozenset({"json_mode"}) _MAPPED_PARAMS_THE_REQUEST_HANDLER_STRIPS: Final = frozenset({"json_mode"})
class _ParamsTheMapperOwns(ExtraItemsTypedDict, total=False, extra_items=object):
pass
_UPLOAD_PARAMS: Final = TypeAdapter(dict[str, object]) _UPLOAD_PARAMS: Final = TypeAdapter(dict[str, object])
@ -710,9 +720,11 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig):
encoded_object_name: Final = encode_s3_object_key_for_url(object_name) encoded_object_name: Final = encode_s3_object_key_for_url(object_name)
# S3 endpoint URL format # S3 endpoint URL format
s3_endpoint_url: Final = ( s3_endpoint_url: Final = require_attributes(
request_params.get("s3_endpoint_url") request_params.get("s3_endpoint_url")
or f"https://s3.{aws_region_name}.{get_aws_dns_suffix(aws_region_name)}" or f"https://s3.{aws_region_name}.{get_aws_dns_suffix(aws_region_name)}",
_SupportsRstrip,
"rstrip",
).rstrip("/") ).rstrip("/")
return f"{s3_endpoint_url}/{bucket_name}/{encoded_object_name}" return f"{s3_endpoint_url}/{bucket_name}/{encoded_object_name}"
@ -1062,7 +1074,8 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig):
from litellm.types.utils import LlmProviders from litellm.types.utils import LlmProviders
messages: Final = map_developer_role_to_system_role(openai_request_body.get("messages", [])) messages: Final = map_developer_role_to_system_role(openai_request_body.get("messages", []))
optional_params: Final = {k: v for k, v in openai_request_body.items() if k not in ["model", "messages"]} request_params: Final[Mapping[str, object]] = openai_request_body
optional_params: Final = {k: v for k, v in request_params.items() if k not in ["model", "messages"]}
# --- Anthropic: use existing AmazonAnthropicClaudeConfig --- # --- Anthropic: use existing AmazonAnthropicClaudeConfig ---
if provider == LlmProviders.ANTHROPIC: if provider == LlmProviders.ANTHROPIC:
@ -1085,8 +1098,8 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig):
custom_llm_provider="bedrock", custom_llm_provider="bedrock",
messages=messages, messages=messages,
model_info=deployment_model_info(litellm_params), model_info=deployment_model_info(litellm_params),
**MappingProxyType( **_ParamsTheMapperOwns(
{ **{
k: v k: v
for k, v in optional_params.items() for k, v in optional_params.items()
if not is_litellm_owned_kwarg(k) or k in _LITELLM_PARAMS_THE_MAPPER_TAKES if not is_litellm_owned_kwarg(k) or k in _LITELLM_PARAMS_THE_MAPPER_TAKES

View file

@ -18,8 +18,10 @@ from typing import TYPE_CHECKING, Any, Final, cast
import httpx import httpx
from httpx._types import RequestFiles from httpx._types import RequestFiles
from pydantic import TypeAdapter
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
from litellm.litellm_core_utils.core_helpers import decoded_json_object, iter_items
from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig
from litellm.llms.bedrock.common_utils import BedrockError from litellm.llms.bedrock.common_utils import BedrockError
from litellm.types.images.main import ImageEditOptionalRequestParams from litellm.types.images.main import ImageEditOptionalRequestParams
@ -38,6 +40,8 @@ if TYPE_CHECKING:
else: else:
LiteLLMLoggingObj = Any LiteLLMLoggingObj = Any
_DECODED_JSON: Final = TypeAdapter(object)
def _nova_canvas_task_body( def _nova_canvas_task_body(
*, *,
@ -401,7 +405,7 @@ class BedrockAmazonNovaCanvasImageEditConfig(BaseImageEditConfig):
json_mode: bool | None = None, json_mode: bool | None = None,
) -> ImageResponse: ) -> ImageResponse:
try: try:
response_data: Final = raw_response.json() response_data: Final = _DECODED_JSON.validate_python(raw_response.json())
except Exception as e: except Exception as e:
raise self.get_error_class( raise self.get_error_class(
error_message=f"Error parsing Nova Canvas image edit response: {e}", error_message=f"Error parsing Nova Canvas image edit response: {e}",
@ -416,18 +420,19 @@ class BedrockAmazonNovaCanvasImageEditConfig(BaseImageEditConfig):
headers=raw_response.headers, headers=raw_response.headers,
) )
images: Final[list[str]] = response_data.get("images") or [] response_object: Final = decoded_json_object(response_data)
images: Final[object] = response_object.get("images") or []
if "errors" in response_data and not images: if "errors" in response_object and not images:
raise self.get_error_class( raise self.get_error_class(
error_message=f"Nova Canvas image edit error: {response_data['errors']}", error_message=f"Nova Canvas image edit error: {response_object['errors']}",
status_code=raw_response.status_code, status_code=raw_response.status_code,
headers=raw_response.headers, headers=raw_response.headers,
) )
# Nova Canvas InvokeModel success body uses "images" and optional "error" (AWS docs); # Nova Canvas InvokeModel success body uses "images" and optional "error" (AWS docs);
# it does not use Stability-style "finish_reasons". # it does not use Stability-style "finish_reasons".
error_msg = response_data.get("message") or response_data.get("error") error_msg = response_object.get("message") or response_object.get("error")
if error_msg and not images: if error_msg and not images:
if not isinstance(error_msg, str): if not isinstance(error_msg, str):
error_msg = str(error_msg) error_msg = str(error_msg)
@ -439,7 +444,7 @@ class BedrockAmazonNovaCanvasImageEditConfig(BaseImageEditConfig):
model_response: Final = ImageResponse() model_response: Final = ImageResponse()
model_response.data = [] model_response.data = []
for image_b64 in images: for image_b64 in iter_items(images):
if image_b64: if image_b64:
model_response.data.append( model_response.data.append(
ImageObject( ImageObject(

View file

@ -22,11 +22,13 @@ API Reference: https://docs.aws.amazon.com/bedrock/latest/userguide/model-parame
""" """
import base64 import base64
from typing import TYPE_CHECKING, Any, Final, cast from typing import TYPE_CHECKING, Any, Final, Protocol, cast, runtime_checkable
import httpx import httpx
from httpx._types import RequestFiles from httpx._types import RequestFiles
from pydantic import TypeAdapter
from litellm.litellm_core_utils.core_helpers import decoded_json_object, iter_items, json_contains
from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig
from litellm.llms.bedrock.common_utils import BedrockError from litellm.llms.bedrock.common_utils import BedrockError
from litellm.types.images.main import ImageEditOptionalRequestParams from litellm.types.images.main import ImageEditOptionalRequestParams
@ -44,6 +46,19 @@ if TYPE_CHECKING:
else: else:
LiteLLMLoggingObj = Any LiteLLMLoggingObj = Any
_DECODED_JSON: Final = TypeAdapter(object)
@runtime_checkable
class _SupportsGetItem(Protocol):
def __getitem__(self, key: str | int, /) -> object: ...
def _subscript(value: object, key: str | int) -> object:
if isinstance(value, _SupportsGetItem):
return value[key]
raise TypeError(f"'{type(value).__name__}' object is not subscriptable")
class BedrockStabilityImageEditConfig(BaseImageEditConfig): class BedrockStabilityImageEditConfig(BaseImageEditConfig):
""" """
@ -299,7 +314,7 @@ class BedrockStabilityImageEditConfig(BaseImageEditConfig):
OpenAI expects: {"data": [{"b64_json": "base64..."}], "created": timestamp} OpenAI expects: {"data": [{"b64_json": "base64..."}], "created": timestamp}
""" """
try: try:
response_data: Final = raw_response.json() response_data: Final = _DECODED_JSON.validate_python(raw_response.json())
except Exception as e: except Exception as e:
raise self.get_error_class( raise self.get_error_class(
error_message=f"Error parsing Bedrock Stability response: {e}", error_message=f"Error parsing Bedrock Stability response: {e}",
@ -308,18 +323,19 @@ class BedrockStabilityImageEditConfig(BaseImageEditConfig):
) )
# Check for errors in response # Check for errors in response
if "errors" in response_data: if json_contains(response_data, "errors"):
raise self.get_error_class( raise self.get_error_class(
error_message=f"Bedrock Stability error: {response_data['errors']}", error_message=f"Bedrock Stability error: {_subscript(response_data, 'errors')}",
status_code=raw_response.status_code, status_code=raw_response.status_code,
headers=raw_response.headers, headers=raw_response.headers,
) )
# Check finish_reasons # Check finish_reasons
finish_reasons: Final = response_data.get("finish_reasons", []) response_fields: Final = decoded_json_object(response_data)
if finish_reasons and finish_reasons[0]: finish_reasons: Final = response_fields.get("finish_reasons", [])
if finish_reasons and _subscript(finish_reasons, 0):
raise self.get_error_class( raise self.get_error_class(
error_message=f"Bedrock Stability error: {finish_reasons[0]}", error_message=f"Bedrock Stability error: {_subscript(finish_reasons, 0)}",
status_code=400, status_code=400,
headers=raw_response.headers, headers=raw_response.headers,
) )
@ -329,9 +345,9 @@ class BedrockStabilityImageEditConfig(BaseImageEditConfig):
model_response.data = [] model_response.data = []
# Extract images from response # Extract images from response
images: Final = response_data.get("images", []) images: Final = response_fields.get("images", [])
if images: if images:
for image_b64 in images: for image_b64 in iter_items(images):
if image_b64: if image_b64:
model_response.data.append( model_response.data.append(
ImageObject( ImageObject(

View file

@ -14,7 +14,7 @@ from pydantic import InstanceOf, TypeAdapter, ValidationError
from typing_extensions import ReadOnly, TypedDict from typing_extensions import ReadOnly, TypedDict
from litellm._logging import verbose_proxy_logger from litellm._logging import verbose_proxy_logger
from litellm.litellm_core_utils.core_helpers import as_str_mapping from litellm.litellm_core_utils.core_helpers import as_str_mapping, decoded_json_object
from litellm.llms.anthropic.chat.guardrail_translation.handler import AnthropicMessagesHandler from litellm.llms.anthropic.chat.guardrail_translation.handler import AnthropicMessagesHandler
from litellm.llms.base_llm.guardrail_translation.base_translation import ( from litellm.llms.base_llm.guardrail_translation.base_translation import (
BaseTranslation, BaseTranslation,
@ -849,6 +849,7 @@ class InvokeStreamFrame:
_JSON_OBJECT: Final = TypeAdapter(Mapping[str, object]) _JSON_OBJECT: Final = TypeAdapter(Mapping[str, object])
_DECODED_JSON: Final = TypeAdapter(object)
_Frame: Final = TypeVar("_Frame", ConverseStreamFrame, InvokeStreamFrame) _Frame: Final = TypeVar("_Frame", ConverseStreamFrame, InvokeStreamFrame)
@ -1298,7 +1299,7 @@ class BedrockPassthroughGuardrailHandler(BaseTranslation):
texts: Sequence[tuple[_StreamGroupKey, str]] = () texts: Sequence[tuple[_StreamGroupKey, str]] = ()
if event_type == "contentBlockDelta": if event_type == "contentBlockDelta":
try: try:
payload_dict: dict[str, object] = json.loads(payload_bytes) payload_dict = decoded_json_object(_DECODED_JSON.validate_python(json.loads(payload_bytes)))
texts = _delta_frame_texts(payload_dict) texts = _delta_frame_texts(payload_dict)
except Exception as e: except Exception as e:
verbose_proxy_logger.debug( verbose_proxy_logger.debug(

View file

@ -1,11 +1,12 @@
from collections.abc import Mapping from collections.abc import Mapping
from copy import deepcopy from copy import deepcopy
from typing import TYPE_CHECKING, Any, Final, cast from typing import TYPE_CHECKING, Any, Final, Protocol, cast, runtime_checkable
from urllib.parse import urlparse from urllib.parse import urlparse
import httpx import httpx
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
from litellm.litellm_core_utils.core_helpers import decoded_json_object
from litellm.litellm_core_utils.url_utils import encode_url_path_segment, join_api_base from litellm.litellm_core_utils.url_utils import encode_url_path_segment, join_api_base
from litellm.llms.base_llm.vector_store.transformation import BaseVectorStoreConfig from litellm.llms.base_llm.vector_store.transformation import BaseVectorStoreConfig
from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM
@ -34,6 +35,32 @@ else:
LiteLLMLoggingObj = Any LiteLLMLoggingObj = Any
@runtime_checkable
class _SupportsSetDefault(Protocol):
def setdefault(self, key: str, default: object, /) -> object: ...
@runtime_checkable
class _SupportsSetItem(Protocol):
def __setitem__(self, key: str, value: object, /) -> None: ...
def _setdefault_target(value: object) -> _SupportsSetDefault:
if isinstance(value, _SupportsSetDefault):
return value
raise AttributeError(f"{type(value).__name__!r} object has no attribute 'setdefault'", name="setdefault", obj=value)
def _item_assignment_target(value: object) -> _SupportsSetItem:
if isinstance(value, _SupportsSetItem):
return value
raise TypeError(f"'{type(value).__name__}' object does not support item assignment")
def _vector_search_configuration(retrieval_config: object) -> Mapping[str, object]:
return decoded_json_object(decoded_json_object(retrieval_config).get("vectorSearchConfiguration", {}))
class BedrockVectorStoreConfig(BaseVectorStoreConfig, BaseAWSLLM): class BedrockVectorStoreConfig(BaseVectorStoreConfig, BaseAWSLLM):
"""Vector store configuration for AWS Bedrock Knowledge Bases.""" """Vector store configuration for AWS Bedrock Knowledge Bases."""
@ -206,7 +233,7 @@ class BedrockVectorStoreConfig(BaseVectorStoreConfig, BaseAWSLLM):
api_base: str, api_base: str,
litellm_logging_obj: LiteLLMLoggingObj, litellm_logging_obj: LiteLLMLoggingObj,
litellm_params: dict, litellm_params: dict,
extra_body: dict[str, Any] | None = None, extra_body: dict[str, object] | None = None,
) -> tuple[str, dict]: ) -> tuple[str, dict]:
if isinstance(query, list): if isinstance(query, list):
query = " ".join(query) query = " ".join(query)
@ -218,30 +245,33 @@ class BedrockVectorStoreConfig(BaseVectorStoreConfig, BaseAWSLLM):
"retrievalQuery": BedrockKBRetrievalQuery(text=query), "retrievalQuery": BedrockKBRetrievalQuery(text=query),
} }
retrieval_config: dict[str, Any] = {} retrieval_config: Final[object] = (
deepcopy(extra_body.get("retrievalConfiguration") or extra_body.get("retrieval_configuration") or {})
if isinstance(extra_body, dict): if isinstance(extra_body, dict)
retrieval_config = deepcopy( else {}
extra_body.get("retrievalConfiguration") or extra_body.get("retrieval_configuration") or {} )
)
max_results: Final = vector_store_search_optional_params.get("max_num_results") max_results: Final = vector_store_search_optional_params.get("max_num_results")
if max_results is not None: if max_results is not None:
existing_number_of_results = retrieval_config.get("vectorSearchConfiguration", {}).get("numberOfResults") existing_number_of_results: Final = _vector_search_configuration(retrieval_config).get("numberOfResults")
if existing_number_of_results is not None and existing_number_of_results != max_results: if existing_number_of_results is not None and existing_number_of_results != max_results:
verbose_logger.debug( verbose_logger.debug(
"Overriding extra_body retrievalConfiguration.vectorSearchConfiguration.numberOfResults (%s) with max_num_results=%s", "Overriding extra_body retrievalConfiguration.vectorSearchConfiguration.numberOfResults (%s) with max_num_results=%s",
existing_number_of_results, existing_number_of_results,
max_results, max_results,
) )
retrieval_config.setdefault("vectorSearchConfiguration", {})["numberOfResults"] = max_results _item_assignment_target(_setdefault_target(retrieval_config).setdefault("vectorSearchConfiguration", {}))[
"numberOfResults"
] = max_results
filters: Final = vector_store_search_optional_params.get("filters") filters: Final = vector_store_search_optional_params.get("filters")
if filters is not None: if filters is not None:
existing_filter: Final = retrieval_config.get("vectorSearchConfiguration", {}).get("filter") existing_filter: Final = _vector_search_configuration(retrieval_config).get("filter")
if existing_filter is not None and existing_filter != filters: if existing_filter is not None and existing_filter != filters:
verbose_logger.debug( verbose_logger.debug(
"Overriding extra_body retrievalConfiguration.vectorSearchConfiguration.filter with filters from vector_store_search_optional_params" "Overriding extra_body retrievalConfiguration.vectorSearchConfiguration.filter with filters from vector_store_search_optional_params"
) )
retrieval_config.setdefault("vectorSearchConfiguration", {})["filter"] = filters _item_assignment_target(_setdefault_target(retrieval_config).setdefault("vectorSearchConfiguration", {}))[
"filter"
] = filters
if retrieval_config: if retrieval_config:
request_body["retrievalConfiguration"] = cast(BedrockKBRetrievalConfiguration, retrieval_config) request_body["retrievalConfiguration"] = cast(BedrockKBRetrievalConfiguration, retrieval_config)
user_context: Final = self._user_context(extra_body=extra_body, litellm_params=litellm_params) user_context: Final = self._user_context(extra_body=extra_body, litellm_params=litellm_params)

View file

@ -10,7 +10,7 @@ API Reference: https://docs.bfl.ai/
import base64 import base64
import time import time
from collections.abc import Mapping from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, Final from typing import TYPE_CHECKING, Any, Final, Protocol
import httpx import httpx
from httpx._types import RequestFiles from httpx._types import RequestFiles
@ -19,6 +19,7 @@ from typing_extensions import NotRequired, ReadOnly, TypedDict
import litellm import litellm
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
from litellm.litellm_core_utils.core_helpers import as_object_list, has_attributes
from litellm.litellm_core_utils.url_utils import async_safe_get, join_api_base, safe_get from litellm.litellm_core_utils.url_utils import async_safe_get, join_api_base, safe_get
from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig
from litellm.secret_managers.main import get_secret_str from litellm.secret_managers.main import get_secret_str
@ -52,6 +53,19 @@ class _BflPolledResult(TypedDict):
_BFL_POLLED_RESULT_ADAPTER: Final = TypeAdapter(_BflPolledResult) _BFL_POLLED_RESULT_ADAPTER: Final = TypeAdapter(_BflPolledResult)
_DECODED_JSON: Final = TypeAdapter(object) _DECODED_JSON: Final = TypeAdapter(object)
_UNTYPED_VALUE: Final = TypeAdapter(object)
class _BinaryReadable(Protocol):
def read(self) -> bytes: ...
class _Tellable(Protocol):
def tell(self) -> int: ...
class _Seekable(Protocol):
def seek(self, offset: int, /) -> object: ...
_BFL_REQUEST_PARAMS: Final = ( _BFL_REQUEST_PARAMS: Final = (
@ -197,7 +211,7 @@ class BlackForestLabsImageEditConfig(BaseImageEditConfig):
def _read_image_bytes( def _read_image_bytes(
self, self,
image: Any, image: object,
depth: int = 0, depth: int = 0,
max_depth: int = DEFAULT_MAX_RECURSE_DEPTH, max_depth: int = DEFAULT_MAX_RECURSE_DEPTH,
) -> bytes: ) -> bytes:
@ -208,9 +222,8 @@ class BlackForestLabsImageEditConfig(BaseImageEditConfig):
) )
if isinstance(image, bytes): if isinstance(image, bytes):
return image return image
elif isinstance(image, list): elif (image_list := as_object_list(image)) is not None:
# If it's a list, take the first image return self._read_image_bytes(image_list[0], depth=depth + 1, max_depth=max_depth)
return self._read_image_bytes(image[0], depth=depth + 1, max_depth=max_depth)
elif isinstance(image, str): elif isinstance(image, str):
if image.startswith(("http://", "https://")): if image.startswith(("http://", "https://")):
response: Final = safe_get(litellm.module_level_client, image, timeout=60.0) response: Final = safe_get(litellm.module_level_client, image, timeout=60.0)
@ -221,13 +234,12 @@ class BlackForestLabsImageEditConfig(BaseImageEditConfig):
"Unsupported image input: plain string values that are not URLs are not accepted. " "Unsupported image input: plain string values that are not URLs are not accepted. "
"Provide image bytes or a file-like object." "Provide image bytes or a file-like object."
) )
elif hasattr(image, "read"): elif has_attributes(image, _BinaryReadable, "read"):
# File-like object pos: Final = image.tell() if has_attributes(image, _Tellable, "tell") else 0
pos: Final = getattr(image, "tell", lambda: 0)() if has_attributes(image, _Seekable, "seek"):
if hasattr(image, "seek"):
image.seek(0) image.seek(0)
data: Final = image.read() data: Final = image.read()
if hasattr(image, "seek"): if has_attributes(image, _Seekable, "seek"):
image.seek(pos) image.seek(pos)
return data return data
else: else:
@ -265,7 +277,7 @@ class BlackForestLabsImageEditConfig(BaseImageEditConfig):
# Handle mask if provided (for inpainting) # Handle mask if provided (for inpainting)
if "mask" in image_edit_optional_request_params: if "mask" in image_edit_optional_request_params:
mask: Final = image_edit_optional_request_params["mask"] mask: Final = _UNTYPED_VALUE.validate_python(image_edit_optional_request_params["mask"])
mask_bytes: Final = self._read_image_bytes(mask) mask_bytes: Final = self._read_image_bytes(mask)
request_body["mask"] = base64.b64encode(mask_bytes).decode("utf-8") request_body["mask"] = base64.b64encode(mask_bytes).decode("utf-8")

View file

@ -6,7 +6,9 @@ from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Final from typing import TYPE_CHECKING, Any, Final
import httpx import httpx
from pydantic import TypeAdapter
from litellm.litellm_core_utils.core_helpers import decoded_json_object
from litellm.litellm_core_utils.exception_mapping_utils import exception_type from litellm.litellm_core_utils.exception_mapping_utils import exception_type
from litellm.litellm_core_utils.logging_utils import track_llm_api_timing from litellm.litellm_core_utils.logging_utils import track_llm_api_timing
from litellm.litellm_core_utils.url_utils import encode_url_path_segments from litellm.litellm_core_utils.url_utils import encode_url_path_segments
@ -36,6 +38,8 @@ else:
# 5 minute timeout (models may need to load) # 5 minute timeout (models may need to load)
STREAMING_TIMEOUT: Final = 60 * 5 STREAMING_TIMEOUT: Final = 60 * 5
_DECODED_JSON: Final = TypeAdapter(object)
class BytezChatConfig(BaseConfig): class BytezChatConfig(BaseConfig):
""" """
@ -194,11 +198,11 @@ class BytezChatConfig(BaseConfig):
) -> ModelResponse: ) -> ModelResponse:
json: Final = raw_response.json() json: Final = raw_response.json()
error: Final = json.get("error") error: Final = decoded_json_object(_DECODED_JSON.validate_python(json)).get("error")
if error is not None: if error is not None:
raise BytezError( raise BytezError(
message=str(json["error"]), message=str(error),
status_code=raw_response.status_code, status_code=raw_response.status_code,
) )

View file

@ -1,6 +1,8 @@
import json import json
from typing import Final, Literal from typing import Final, Literal
from pydantic import TypeAdapter
from litellm.llms.base_llm.base_utils import BaseLLMModelInfo from litellm.llms.base_llm.base_utils import BaseLLMModelInfo
from litellm.llms.base_llm.chat.transformation import BaseLLMException from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.types.llms.cohere import CohereV2StreamedToolCall from litellm.types.llms.cohere import CohereV2StreamedToolCall
@ -12,6 +14,8 @@ from litellm.types.utils import (
ProviderSpecificModelInfo, ProviderSpecificModelInfo,
) )
_DECODED_JSON: Final = TypeAdapter(object)
class CohereError(BaseLLMException): class CohereError(BaseLLMException):
def __init__(self, status_code, message): def __init__(self, status_code, message):
@ -190,8 +194,10 @@ class ModelResponseIterator:
if index != -1: if index != -1:
str_line = str_line[index:] str_line = str_line[index:]
data_json: Final = json.loads(str_line) data_json: Final = _DECODED_JSON.validate_python(json.loads(str_line))
return self.chunk_parser(chunk=data_json) if isinstance(data_json, dict):
return self.chunk_parser(chunk=data_json)
raise AttributeError(f"{type(data_json).__name__!r} object has no attribute 'get'", name="get", obj=data_json)
# Async iterator # Async iterator
def __aiter__(self): def __aiter__(self):

View file

@ -153,7 +153,7 @@ from litellm.types.containers.main import (
) )
from litellm.types.decisions import DecisionsIRRequest, DecisionsIRResponse from litellm.types.decisions import DecisionsIRRequest, DecisionsIRResponse
from litellm.types.extraction import ExtractionResponse from litellm.types.extraction import ExtractionResponse
from litellm.types.files import StreamingMediaUploadConfig, TwoStepFileUploadConfig from litellm.types.files import StreamingMediaUploadConfig, TwoStepFileUploadConfig, TwoStepFileUploadRequest
from litellm.types.integrations.custom_logger import ( from litellm.types.integrations.custom_logger import (
NON_CODE_INTERPRETER_INTERCEPTION_INTERNAL_PREFIXES, NON_CODE_INTERPRETER_INTERCEPTION_INTERNAL_PREFIXES,
AgenticLoopLimitError, AgenticLoopLimitError,
@ -188,6 +188,7 @@ from litellm.types.responses.main import DeleteResponseResult
from litellm.types.responses.streaming_websocket import ResponsesWebSocketRequestDefaults from litellm.types.responses.streaming_websocket import ResponsesWebSocketRequestDefaults
from litellm.types.router import GenericLiteLLMParams from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import ( from litellm.types.utils import (
AgenticLoopParams,
CallTypes, CallTypes,
EmbeddingResponse, EmbeddingResponse,
FileTypes, FileTypes,
@ -239,6 +240,10 @@ def _client_request_headers(
return _merge_headers_case_insensitive(optional_headers or {}, extra_headers) return _merge_headers_case_insensitive(optional_headers or {}, extra_headers)
def _requested_stream(response_api_optional_request_params: Mapping[str, object]) -> object:
return response_api_optional_request_params.get("stream", False)
def _rust_responses_websocket_enabled( def _rust_responses_websocket_enabled(
custom_llm_provider: str | None, custom_llm_provider: str | None,
) -> bool: ) -> bool:
@ -386,12 +391,19 @@ class _SignedBodyKwargs(TypedDict, total=False):
json: ReadOnly[dict[str, object]] json: ReadOnly[dict[str, object]]
class _AgenticLoopParamsView(TypedDict):
agentic_loop_params: ReadOnly[AgenticLoopParams]
_GOAWAY_RETRY_MESSAGE: Final = "Vector store search connection was closed by an HTTP/2 GOAWAY, retrying once" _GOAWAY_RETRY_MESSAGE: Final = "Vector store search connection was closed by an HTTP/2 GOAWAY, retrying once"
def _closed_by_http2_goaway(error: httpx.RemoteProtocolError) -> bool: def _closed_by_http2_goaway(error: httpx.RemoteProtocolError) -> bool:
cause: Final = error.__cause__ cause: Final = error.__cause__
return cause is not None and any(isinstance(arg, ConnectionTerminated) for arg in cause.args) if cause is None:
return False
cause_args: Final[tuple[object, ...]] = cause.args
return any(isinstance(arg, ConnectionTerminated) for arg in cause_args)
async def _asend_with_goaway_retry(send: Callable[[], Awaitable[httpx.Response]]) -> httpx.Response: async def _asend_with_goaway_retry(send: Callable[[], Awaitable[httpx.Response]]) -> httpx.Response:
@ -433,8 +445,12 @@ def _signed_body_kwargs(*, signed_body: bytes | None, data: dict[str, object]) -
return {"json": data} return {"json": data}
def _response_content_type(headers: Mapping[str, str]) -> str:
return headers.get("content-type", "")
def _answered_json(response: httpx.Response) -> bool: def _answered_json(response: httpx.Response) -> bool:
return "application/json" in response.headers.get("content-type", "").lower() return "application/json" in _response_content_type(response.headers).lower()
def _stream_query_params(stream: bool, starting_after: int | None) -> Mapping[str, object]: def _stream_query_params(stream: bool, starting_after: int | None) -> Mapping[str, object]:
@ -3064,7 +3080,7 @@ class BaseLLMHTTPHandler:
} }
# Check if streaming is requested # Check if streaming is requested
stream = response_api_optional_request_params.get("stream", False) stream = _requested_stream(response_api_optional_request_params)
caller_requested_stream: Final = bool(stream or (extra_body or {}).get("stream")) caller_requested_stream: Final = bool(stream or (extra_body or {}).get("stream"))
api_base: Final = responses_api_provider_config.get_complete_url( api_base: Final = responses_api_provider_config.get_complete_url(
@ -4136,7 +4152,7 @@ class BaseLLMHTTPHandler:
): ):
# Handle pre-signed requests (e.g., from Bedrock S3 uploads) # Handle pre-signed requests (e.g., from Bedrock S3 uploads)
# Type narrowing: this is a plain dict, not TwoStepFileUploadConfig # Type narrowing: this is a plain dict, not TwoStepFileUploadConfig
presigned_request: Final = cast(dict[str, Any], transformed_request) presigned_request: Final = cast(TwoStepFileUploadRequest, transformed_request)
upload_response = getattr(sync_httpx_client, presigned_request["method"].lower())( upload_response = getattr(sync_httpx_client, presigned_request["method"].lower())(
url=presigned_request["url"], url=presigned_request["url"],
headers=presigned_request["headers"], headers=presigned_request["headers"],
@ -4350,7 +4366,7 @@ class BaseLLMHTTPHandler:
): ):
# Handle pre-signed requests (e.g., from Bedrock S3 uploads) # Handle pre-signed requests (e.g., from Bedrock S3 uploads)
# Type narrowing: this is a plain dict, not TwoStepFileUploadConfig # Type narrowing: this is a plain dict, not TwoStepFileUploadConfig
presigned_request: Final = cast(dict[str, Any], transformed_request) presigned_request: Final = cast(TwoStepFileUploadRequest, transformed_request)
upload_response = await getattr(async_httpx_client, presigned_request["method"].lower())( upload_response = await getattr(async_httpx_client, presigned_request["method"].lower())(
url=presigned_request["url"], url=presigned_request["url"],
headers=presigned_request["headers"], headers=presigned_request["headers"],
@ -6129,8 +6145,10 @@ class BaseLLMHTTPHandler:
full_model_name = model full_model_name = model
if logging_obj is not None: if logging_obj is not None:
agentic_params: Final[Mapping[str, object]] = logging_obj.model_call_details.get("agentic_loop_params", {}) agentic_view: Final[_AgenticLoopParamsView] = {
full_model_name = cast(str, agentic_params.get("model", model)) "agentic_loop_params": logging_obj.model_call_details.get("agentic_loop_params", {})
}
full_model_name = agentic_view["agentic_loop_params"].get("model", model)
optional_params: Final = dict(anthropic_messages_optional_request_params) optional_params: Final = dict(anthropic_messages_optional_request_params)
optional_params.update(patch.optional_params) optional_params.update(patch.optional_params)
@ -11116,7 +11134,7 @@ class BaseLLMHTTPHandler:
extra_body: dict[str, object] | None = None, extra_body: dict[str, object] | None = None,
timeout: float | httpx.Timeout | None = None, timeout: float | httpx.Timeout | None = None,
client: HTTPHandler | AsyncHTTPHandler | None = None, client: HTTPHandler | AsyncHTTPHandler | None = None,
): ) -> Mapping[str, object]:
if client is None or not isinstance(client, AsyncHTTPHandler): if client is None or not isinstance(client, AsyncHTTPHandler):
async_httpx_client = get_async_httpx_client( async_httpx_client = get_async_httpx_client(
llm_provider=litellm.LlmProviders(custom_llm_provider), llm_provider=litellm.LlmProviders(custom_llm_provider),
@ -11407,7 +11425,7 @@ class BaseLLMHTTPHandler:
extra_body: dict[str, object] | None = None, extra_body: dict[str, object] | None = None,
timeout: float | httpx.Timeout | None = None, timeout: float | httpx.Timeout | None = None,
client: HTTPHandler | AsyncHTTPHandler | None = None, client: HTTPHandler | AsyncHTTPHandler | None = None,
): ) -> Mapping[str, object]:
if client is None or not isinstance(client, AsyncHTTPHandler): if client is None or not isinstance(client, AsyncHTTPHandler):
async_httpx_client = get_async_httpx_client( async_httpx_client = get_async_httpx_client(
llm_provider=litellm.LlmProviders(custom_llm_provider), llm_provider=litellm.LlmProviders(custom_llm_provider),

View file

@ -11,12 +11,13 @@ Endpoint
Docs - https://help.aliyun.com/zh/model-studio/text-embedding-synchronous-api Docs - https://help.aliyun.com/zh/model-studio/text-embedding-synchronous-api
""" """
from typing import Final from typing import Final, Protocol, runtime_checkable
import httpx import httpx
from pydantic import ConfigDict, TypeAdapter, with_config from pydantic import ConfigDict, TypeAdapter, with_config
from typing_extensions import NotRequired, ReadOnly, TypedDict from typing_extensions import NotRequired, ReadOnly, TypedDict
from litellm.litellm_core_utils.core_helpers import json_contains
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.litellm_core_utils.url_utils import ensure_api_route from litellm.litellm_core_utils.url_utils import ensure_api_route
from litellm.llms.base_llm.chat.transformation import BaseLLMException from litellm.llms.base_llm.chat.transformation import BaseLLMException
@ -29,6 +30,19 @@ from ..common_utils import DashScopeError, resolve_dashscope_openai_api_base
DEFAULT_API_BASE: Final = "https://dashscope.aliyuncs.com/compatible-mode/v1" DEFAULT_API_BASE: Final = "https://dashscope.aliyuncs.com/compatible-mode/v1"
_DECODED_JSON: Final = TypeAdapter(object)
@runtime_checkable
class _SupportsGetItem(Protocol):
def __getitem__(self, key: str, /) -> object: ...
def _subscript(value: object, key: str) -> object:
if isinstance(value, _SupportsGetItem):
return value[key]
raise TypeError(f"'{type(value).__name__}' object is not subscriptable")
@with_config(ConfigDict(extra="allow", strict=True)) @with_config(ConfigDict(extra="allow", strict=True))
class _DashScopeUsage(TypedDict, total=False): class _DashScopeUsage(TypedDict, total=False):
@ -149,7 +163,7 @@ class DashScopeEmbeddingConfig(BaseEmbeddingConfig):
litellm_params: dict, litellm_params: dict,
) -> EmbeddingResponse: ) -> EmbeddingResponse:
try: try:
response_json: Final = raw_response.json() response_json: Final = _DECODED_JSON.validate_python(raw_response.json())
except Exception as e: except Exception as e:
raise DashScopeError( raise DashScopeError(
status_code=raw_response.status_code, status_code=raw_response.status_code,
@ -163,8 +177,8 @@ class DashScopeEmbeddingConfig(BaseEmbeddingConfig):
original_response=response_json, original_response=response_json,
) )
if "error" in response_json: if json_contains(response_json, "error"):
error: Final[object] = response_json["error"] error: Final = _subscript(response_json, "error")
message: Final = error.get("message", str(error)) if isinstance(error, dict) else str(error) message: Final = error.get("message", str(error)) if isinstance(error, dict) else str(error)
raise DashScopeError( raise DashScopeError(
status_code=raw_response.status_code, status_code=raw_response.status_code,

View file

@ -4,13 +4,14 @@ Calls DataForSEO SERP API to search the web.
DataForSEO API Reference: https://docs.dataforseo.com/v3/serp/google/organic/live/advanced/?bash DataForSEO API Reference: https://docs.dataforseo.com/v3/serp/google/organic/live/advanced/?bash
""" """
from collections.abc import Iterable, Mapping from collections.abc import Iterable, Mapping, Sized
from typing import Final, Literal from typing import Final, Literal, Protocol, runtime_checkable
import httpx import httpx
from pydantic import ConfigDict, TypeAdapter from pydantic import ConfigDict, TypeAdapter
from litellm.constants import DEFAULT_DATAFORSEO_LOCATION_CODE from litellm.constants import DEFAULT_DATAFORSEO_LOCATION_CODE
from litellm.litellm_core_utils.core_helpers import decoded_json_object, json_contains
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.llms.base_llm.search.transformation import ( from litellm.llms.base_llm.search.transformation import (
BaseSearchConfig, BaseSearchConfig,
@ -21,6 +22,24 @@ from litellm.secret_managers.main import get_secret_str
_JSON_OBJECT: Final = TypeAdapter(Mapping[str, object], config=ConfigDict(hide_input_in_errors=True)) _JSON_OBJECT: Final = TypeAdapter(Mapping[str, object], config=ConfigDict(hide_input_in_errors=True))
_JSON_VALUES: Final = TypeAdapter(Iterable[object], config=ConfigDict(hide_input_in_errors=True)) _JSON_VALUES: Final = TypeAdapter(Iterable[object], config=ConfigDict(hide_input_in_errors=True))
_DECODED_JSON: Final = TypeAdapter(object)
@runtime_checkable
class _SupportsGetItem(Protocol):
def __getitem__(self, key: str | int, /) -> object: ...
def _subscript(value: object, key: str | int) -> object:
if isinstance(value, _SupportsGetItem):
return value[key]
raise TypeError(f"'{type(value).__name__}' object is not subscriptable")
def _json_len(value: object) -> int:
if isinstance(value, Sized):
return len(value)
raise TypeError(f"object of type '{type(value).__name__}' has no len()")
class DataForSEOSearchConfig(BaseSearchConfig): class DataForSEOSearchConfig(BaseSearchConfig):
@ -193,20 +212,20 @@ class DataForSEOSearchConfig(BaseSearchConfig):
Returns: Returns:
SearchResponse with standardized format SearchResponse with standardized format
""" """
response_json: Final = raw_response.json() response_json: Final = _DECODED_JSON.validate_python(raw_response.json())
# Transform results to SearchResult objects # Transform results to SearchResult objects
results: Final = [] results: Final = []
# DataForSEO wraps results in tasks array # DataForSEO wraps results in tasks array
if "tasks" in response_json and len(response_json["tasks"]) > 0: if json_contains(response_json, "tasks") and _json_len(_subscript(response_json, "tasks")) > 0:
task: Final = response_json["tasks"][0] task: Final = decoded_json_object(_subscript(_subscript(response_json, "tasks"), 0))
# Check if task was successful # Check if task was successful
if task.get("status_code") == 20000 and "result" in task: if task.get("status_code") == 20000 and "result" in task:
# Result is an array, take first element # Result is an array, take first element
if len(task["result"]) > 0: if _json_len(task["result"]) > 0:
result: Final = _JSON_OBJECT.validate_python(task["result"][0]) result: Final = _JSON_OBJECT.validate_python(_subscript(task["result"], 0))
# Items contain the actual search results # Items contain the actual search results
for raw_item in _JSON_VALUES.validate_python(result.get("items", [])): for raw_item in _JSON_VALUES.validate_python(result.get("items", [])):

View file

@ -4,10 +4,11 @@ import json
import math import math
from collections.abc import Iterable, Mapping, Sequence from collections.abc import Iterable, Mapping, Sequence
from types import MappingProxyType from types import MappingProxyType
from typing import Any, Final, cast from typing import Final, cast
import httpx import httpx
from pydantic import JsonValue, TypeAdapter from pydantic import JsonValue, TypeAdapter
from typing_extensions import TypedDict as ExtraItemsTypedDict
import litellm import litellm
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH
@ -510,6 +511,13 @@ def get_api_key_from_env() -> str | None:
return get_secret_str("GOOGLE_API_KEY") or get_secret_str("GEMINI_API_KEY") return get_secret_str("GOOGLE_API_KEY") or get_secret_str("GEMINI_API_KEY")
_COUNT_TOKENS_REQUEST_FIELDS: Final = frozenset({"model", "contents", "system_instruction", "tools"})
class _ForwardedLitellmParams(ExtraItemsTypedDict, total=False, extra_items=object):
pass
class GoogleAIStudioTokenCounter(BaseTokenCounter): class GoogleAIStudioTokenCounter(BaseTokenCounter):
"""Token counter implementation for Google AI Studio provider.""" """Token counter implementation for Google AI Studio provider."""
@ -526,7 +534,7 @@ class GoogleAIStudioTokenCounter(BaseTokenCounter):
model_to_use: str, model_to_use: str,
messages: list[dict[str, object]] | None, messages: list[dict[str, object]] | None,
contents: list[dict[str, object]] | None, contents: list[dict[str, object]] | None,
deployment: dict[str, Any] | None = None, deployment: dict[str, object] | None = None,
request_model: str = "", request_model: str = "",
tools: list[dict[str, object]] | None = None, tools: list[dict[str, object]] | None = None,
system: object | None = None, system: object | None = None,
@ -537,6 +545,7 @@ class GoogleAIStudioTokenCounter(BaseTokenCounter):
) -> TokenCountResponse | None: ) -> TokenCountResponse | None:
import copy import copy
from litellm.litellm_core_utils.core_helpers import decoded_json_object
from litellm.llms.anthropic.messages.context_management.inference_view import ( from litellm.llms.anthropic.messages.context_management.inference_view import (
InferenceRequestViewError, InferenceRequestViewError,
count_context_edits, count_context_edits,
@ -553,11 +562,12 @@ class GoogleAIStudioTokenCounter(BaseTokenCounter):
vertex_gemini_count_context, vertex_gemini_count_context,
) )
litellm_params: Final = decoded_json_object((deployment or {}).get("litellm_params", {}))
request_view: Final = await inference_request_view( request_view: Final = await inference_request_view(
messages or [], messages or [],
system, system,
messages_format, messages_format,
count_context_edits(context_management, tools, (deployment or {}).get("litellm_params", {}), model_to_use), count_context_edits(context_management, tools, litellm_params, model_to_use),
) )
if isinstance(request_view, InferenceRequestViewError): if isinstance(request_view, InferenceRequestViewError):
return count_context_error_response(request_view, request_model, model_to_use) return count_context_error_response(request_view, request_model, model_to_use)
@ -570,8 +580,7 @@ class GoogleAIStudioTokenCounter(BaseTokenCounter):
if blocks_error is not None: if blocks_error is not None:
return count_context_error_response(blocks_error, request_model, model_to_use) return count_context_error_response(blocks_error, request_model, model_to_use)
deployment = deployment or {} count_tokens_params_request: Final = copy.deepcopy(litellm_params)
count_tokens_params_request: Final = copy.deepcopy(deployment.get("litellm_params", {}))
resolved_contents: Final = ( resolved_contents: Final = (
contents contents
if contents is not None if contents is not None
@ -581,16 +590,15 @@ class GoogleAIStudioTokenCounter(BaseTokenCounter):
) )
if isinstance(resolved_contents, VertexGeminiCountContextError): if isinstance(resolved_contents, VertexGeminiCountContextError):
return count_context_error_response(resolved_contents, request_model, model_to_use) return count_context_error_response(resolved_contents, request_model, model_to_use)
count_tokens_params: Final = {
"model": model_to_use,
"contents": resolved_contents,
"system_instruction": count_context.system_instruction,
"tools": count_context.tools,
}
count_tokens_params_request.update(count_tokens_params)
try: try:
result: Final = await GoogleAIStudioTokenCounter().acount_tokens( result: Final = await GoogleAIStudioTokenCounter().acount_tokens(
**count_tokens_params_request, **_ForwardedLitellmParams(
**{k: v for k, v in count_tokens_params_request.items() if k not in _COUNT_TOKENS_REQUEST_FIELDS}
),
model=model_to_use,
contents=resolved_contents,
system_instruction=count_context.system_instruction,
tools=count_context.tools,
) )
except litellm.APIError as error: except litellm.APIError as error:
return provider_error_response(error, request_model, model_to_use) return provider_error_response(error, request_model, model_to_use)

View file

@ -1,11 +1,12 @@
from collections.abc import Mapping, Sequence from collections.abc import Iterable, Mapping, Sequence
from typing import TYPE_CHECKING, Any, Final from typing import TYPE_CHECKING, Any, Final, Protocol, runtime_checkable
import httpx import httpx
from pydantic import ConfigDict, RootModel, with_config from pydantic import ConfigDict, RootModel, with_config
from typing_extensions import NotRequired, ReadOnly, TypedDict from typing_extensions import NotRequired, ReadOnly, TypedDict
import litellm import litellm
from litellm.litellm_core_utils.core_helpers import has_attributes, iter_items, json_contains
from litellm.litellm_core_utils.url_utils import join_api_base from litellm.litellm_core_utils.url_utils import join_api_base
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, get_async_httpx_client from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, get_async_httpx_client
from litellm.types.utils import LlmProviders from litellm.types.utils import LlmProviders
@ -26,8 +27,44 @@ class _GeminiCountTokensResponseRoot(RootModel[_GeminiCountTokensResponse]):
model_config = ConfigDict(frozen=True, hide_input_in_errors=True) model_config = ConfigDict(frozen=True, hide_input_in_errors=True)
@runtime_checkable
class _SupportsGetItem(Protocol):
def __getitem__(self, key: str, /) -> object: ...
@runtime_checkable
class _SupportsSetItem(Protocol):
def __setitem__(self, key: str, value: object, /) -> None: ...
class _KeywordArguments(Protocol):
def keys(self) -> Iterable[str]: ...
def __getitem__(self, key: str, /) -> object: ...
def _subscript(value: object, key: str) -> object:
if isinstance(value, _SupportsGetItem):
return value[key]
raise TypeError(f"'{type(value).__name__}' object is not subscriptable")
def _item_assignment_target(value: object) -> _SupportsSetItem:
if isinstance(value, _SupportsSetItem):
return value
raise TypeError(f"'{type(value).__name__}' object does not support item assignment")
def _function_response_kwargs(value: object) -> _KeywordArguments:
if has_attributes(value, _KeywordArguments, "keys"):
return value
raise TypeError(
f"google.genai.types.FunctionResponse() argument after ** must be a mapping, not {type(value).__name__}"
)
class GoogleAIStudioTokenCounter: class GoogleAIStudioTokenCounter:
def _clean_contents_for_gemini_api(self, contents: Any) -> object: def _clean_contents_for_gemini_api(self, contents: Sequence[Mapping[str, object]] | None) -> object:
""" """
Clean up contents to remove unsupported fields for the Gemini API. Clean up contents to remove unsupported fields for the Gemini API.
@ -52,12 +89,14 @@ class GoogleAIStudioTokenCounter:
for content in cleaned_contents: for content in cleaned_contents:
parts = content["parts"] parts = content["parts"]
for part in parts: for part in iter_items(parts):
if "functionResponse" in part: if json_contains(part, "functionResponse"):
function_response_data = part["functionResponse"] function_response_data = _subscript(part, "functionResponse")
function_response_part = FunctionResponse(**function_response_data) function_response_part = FunctionResponse(**_function_response_kwargs(function_response_data))
function_response_part.id = None function_response_part.id = None
part["functionResponse"] = function_response_part.model_dump(exclude_none=True) _item_assignment_target(part)["functionResponse"] = function_response_part.model_dump(
exclude_none=True
)
return cleaned_contents return cleaned_contents

View file

@ -9,13 +9,14 @@ Per OpenAPI spec (https://ai.google.dev/static/api/interactions.openapi.json):
Requests use Api-Revision 2026-05-20 (`steps` schema). Requests use Api-Revision 2026-05-20 (`steps` schema).
""" """
from collections.abc import Iterable, Mapping
from typing import TYPE_CHECKING, Any, Final, Protocol, TypeAlias from typing import TYPE_CHECKING, Any, Final, Protocol, TypeAlias
import httpx import httpx
from typing_extensions import ReadOnly, TypedDict from typing_extensions import ReadOnly, TypedDict
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
from litellm.litellm_core_utils.core_helpers import drops_unsupported_params, process_response_headers from litellm.litellm_core_utils.core_helpers import drops_unsupported_params, has_attributes, process_response_headers
from litellm.litellm_core_utils.url_utils import encode_url_path_segment, join_api_base from litellm.litellm_core_utils.url_utils import encode_url_path_segment, join_api_base
from litellm.llms.base_llm.interactions.transformation import BaseInteractionsAPIConfig from litellm.llms.base_llm.interactions.transformation import BaseInteractionsAPIConfig
from litellm.llms.gemini.common_utils import GeminiError, GeminiModelInfo from litellm.llms.gemini.common_utils import GeminiError, GeminiModelInfo
@ -83,6 +84,18 @@ class _CancelPayloadSource(Protocol):
def json(self) -> _CancelPayload: ... def json(self) -> _CancelPayload: ...
class _ImageConfigItems(Protocol):
def keys(self) -> Iterable[str]: ...
def __getitem__(self, key: str, /) -> object: ...
def _image_config_items(image_config: object) -> _ImageConfigItems:
if has_attributes(image_config, _ImageConfigItems, "keys"):
return image_config
raise TypeError(f"'{type(image_config).__name__}' object is not a mapping")
def _interaction_body(response: _InteractionPayloadSource) -> _InteractionPayload: def _interaction_body(response: _InteractionPayloadSource) -> _InteractionPayload:
"""Decode the body of an Interactions API interaction response.""" """Decode the body of an Interactions API interaction response."""
return response.json() return response.json()
@ -241,7 +254,7 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
request_body["response_format"] = response_format request_body["response_format"] = response_format
# image_config moves out of generation_config into response_format. # image_config moves out of generation_config into response_format.
generation_config: dict[str, Any] | None = optional_params.get("generation_config") generation_config: Mapping[str, object] | None = optional_params.get("generation_config")
if generation_config is not None: if generation_config is not None:
image_config = None image_config = None
if isinstance(generation_config, dict): if isinstance(generation_config, dict):
@ -255,7 +268,7 @@ class GoogleAIStudioInteractionsConfig(BaseInteractionsAPIConfig):
if image_config is not None: if image_config is not None:
# Move image_config to response_format with type=image. # Move image_config to response_format with type=image.
image_rf: Final[_JsonObject] = {"type": "image", **image_config} image_rf: Final[_JsonObject] = {"type": "image", **_image_config_items(image_config)}
existing_rf: Final = request_body.get("response_format") existing_rf: Final = request_body.get("response_format")
if existing_rf is None: if existing_rf is None:
request_body["response_format"] = image_rf request_body["response_format"] = image_rf

View file

@ -4,9 +4,12 @@ GigaChat Streaming Response Handler
import json import json
import uuid import uuid
from collections.abc import Mapping, Sequence from collections.abc import Awaitable, Mapping, Sequence
from typing import Any, Final from typing import Final, Protocol
from pydantic import TypeAdapter
from litellm.litellm_core_utils.core_helpers import require_attributes
from litellm.llms.gigachat.utils import convert_usage from litellm.llms.gigachat.utils import convert_usage
from litellm.types.llms.openai import ( from litellm.types.llms.openai import (
ChatCompletionToolCallChunk, ChatCompletionToolCallChunk,
@ -14,13 +17,27 @@ from litellm.types.llms.openai import (
) )
from litellm.types.utils import ChatCompletionUsageBlock, GenericStreamingChunk from litellm.types.utils import ChatCompletionUsageBlock, GenericStreamingChunk
_DECODED_JSON: Final = TypeAdapter(object)
class _SyncChunkSource(Protocol):
def __next__(self) -> object: ...
class _AsyncChunkSource(Protocol):
def __anext__(self) -> Awaitable[object]: ...
class _SupportsGet(Protocol):
def get(self, key: str, /) -> object: ...
class GigaChatModelResponseIterator: class GigaChatModelResponseIterator:
"""Iterator for GigaChat streaming responses.""" """Iterator for GigaChat streaming responses."""
def __init__( def __init__(
self, self,
streaming_response: Any, streaming_response: object,
sync_stream: bool, sync_stream: bool,
json_mode: bool | None = False, json_mode: bool | None = False,
): ):
@ -28,7 +45,7 @@ class GigaChatModelResponseIterator:
self.response_iterator = self.streaming_response self.response_iterator = self.streaming_response
self.json_mode = json_mode self.json_mode = json_mode
def chunk_parser(self, chunk: Mapping[str, object]) -> GenericStreamingChunk: def chunk_parser(self, chunk: _SupportsGet) -> GenericStreamingChunk:
"""Parse a single streaming chunk from GigaChat.""" """Parse a single streaming chunk from GigaChat."""
choices: Sequence = chunk.get("choices") or () choices: Sequence = chunk.get("choices") or ()
if not choices: if not choices:
@ -106,14 +123,14 @@ class GigaChatModelResponseIterator:
def __next__(self) -> GenericStreamingChunk: def __next__(self) -> GenericStreamingChunk:
try: try:
chunk = self.response_iterator.__next__() chunk: Final = require_attributes(self.response_iterator, _SyncChunkSource, "__next__").__next__()
if isinstance(chunk, str): if isinstance(chunk, str):
# Parse SSE format: data: {...} # Parse SSE format: data: {...}
chunk = chunk.removeprefix("data: ") payload: Final = chunk.removeprefix("data: ")
if chunk.strip() == "[DONE]": if payload.strip() == "[DONE]":
raise StopIteration raise StopIteration
try: try:
chunk = json.loads(chunk) decoded: Final = _DECODED_JSON.validate_python(json.loads(payload))
except json.JSONDecodeError: except json.JSONDecodeError:
return GenericStreamingChunk( return GenericStreamingChunk(
text="", text="",
@ -123,7 +140,8 @@ class GigaChatModelResponseIterator:
usage=None, usage=None,
index=0, index=0,
) )
return self.chunk_parser(chunk) return self.chunk_parser(require_attributes(decoded, _SupportsGet, "get"))
return self.chunk_parser(require_attributes(chunk, _SupportsGet, "get"))
except StopIteration: except StopIteration:
raise raise
@ -132,14 +150,14 @@ class GigaChatModelResponseIterator:
async def __anext__(self) -> GenericStreamingChunk: async def __anext__(self) -> GenericStreamingChunk:
try: try:
chunk = await self.response_iterator.__anext__() chunk: Final = await require_attributes(self.response_iterator, _AsyncChunkSource, "__anext__").__anext__()
if isinstance(chunk, str): if isinstance(chunk, str):
# Parse SSE format # Parse SSE format
chunk = chunk.removeprefix("data: ") payload: Final = chunk.removeprefix("data: ")
if chunk.strip() == "[DONE]": if payload.strip() == "[DONE]":
raise StopAsyncIteration raise StopAsyncIteration
try: try:
chunk = json.loads(chunk) decoded: Final = _DECODED_JSON.validate_python(json.loads(payload))
except json.JSONDecodeError: except json.JSONDecodeError:
return GenericStreamingChunk( return GenericStreamingChunk(
text="", text="",
@ -149,6 +167,7 @@ class GigaChatModelResponseIterator:
usage=None, usage=None,
index=0, index=0,
) )
return self.chunk_parser(chunk) return self.chunk_parser(require_attributes(decoded, _SupportsGet, "get"))
return self.chunk_parser(require_attributes(chunk, _SupportsGet, "get"))
except StopAsyncIteration: except StopAsyncIteration:
raise raise

View file

@ -1,11 +1,13 @@
import json import json
import os import os
from typing import Final, Literal, Protocol, get_args from collections.abc import Iterator
from typing import Final, Literal, Protocol, get_args, runtime_checkable
import httpx import httpx
from pydantic import TypeAdapter from pydantic import TypeAdapter
import litellm import litellm
from litellm.litellm_core_utils.core_helpers import as_object_list, iter_items, json_contains
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.llms.custom_httpx.http_handler import ( from litellm.llms.custom_httpx.http_handler import (
AsyncHTTPHandler, AsyncHTTPHandler,
@ -38,6 +40,28 @@ class _SupportsTokenEncode(Protocol):
def encode(self, text: str) -> list[int]: ... def encode(self, text: str) -> list[int]: ...
@runtime_checkable
class _SupportsGetItem(Protocol):
def __getitem__(self, key: str | int, /) -> object: ...
def _subscript(value: object, key: str | int) -> object:
if isinstance(value, _SupportsGetItem):
return value[key]
raise TypeError(f"'{type(value).__name__}' object is not subscriptable")
def _unpack_pair(value: object) -> tuple[object, object]:
if not hasattr(value, "__iter__") and not hasattr(value, "__getitem__"):
raise TypeError(f"cannot unpack non-iterable {type(value).__name__} object")
first, second = iter_items(value)
return first, second
def _pairs(value: object) -> Iterator[tuple[object, object]]:
return (_unpack_pair(item) for item in iter_items(value))
def get_hf_task_embedding_for_model( def get_hf_task_embedding_for_model(
model: str, task_type: str | None, api_base: str, timeout: float | httpx.Timeout | None = None model: str, task_type: str | None, api_base: str, timeout: float | httpx.Timeout | None = None
) -> object: ) -> object:
@ -183,15 +207,15 @@ class HuggingFaceEmbedding(BaseLLM):
def _process_embedding_response( def _process_embedding_response(
self, self,
embeddings: dict, embeddings: object,
model_response: EmbeddingResponse, model_response: EmbeddingResponse,
model: str, model: str,
input: list, input: list,
encoding: _SupportsTokenEncode, encoding: _SupportsTokenEncode,
) -> EmbeddingResponse: ) -> EmbeddingResponse:
output_data: Final = [] output_data: Final = []
if "similarities" in embeddings: if json_contains(embeddings, "similarities"):
for idx, embedding in embeddings["similarities"]: for idx, embedding in _pairs(_subscript(embeddings, "similarities")):
output_data.append( output_data.append(
{ {
"object": "embedding", "object": "embedding",
@ -200,8 +224,9 @@ class HuggingFaceEmbedding(BaseLLM):
} }
) )
else: else:
for idx, embedding in enumerate(embeddings): for idx, embedding in enumerate(iter_items(embeddings)):
if isinstance(embedding, float) or isinstance(embedding, list) and isinstance(embedding[0], float): embedding_list = as_object_list(embedding)
if isinstance(embedding, float) or embedding_list is not None and isinstance(embedding_list[0], float):
output_data.append( output_data.append(
{ {
"object": "embedding", "object": "embedding",
@ -214,7 +239,7 @@ class HuggingFaceEmbedding(BaseLLM):
{ {
"object": "embedding", "object": "embedding",
"index": idx, "index": idx,
"embedding": embedding[0][0], # flatten list returned from hf "embedding": _subscript(_subscript(embedding, 0), 0), # flatten list returned from hf
} }
) )
model_response.object = "list" model_response.object = "list"
@ -288,10 +313,10 @@ class HuggingFaceEmbedding(BaseLLM):
original_response=response, original_response=response,
) )
embeddings: Final = response.json() embeddings: Final = _DECODED_JSON.validate_python(response.json())
if "error" in embeddings: if json_contains(embeddings, "error"):
raise HuggingFaceError(status_code=500, message=embeddings["error"]) raise HuggingFaceError(status_code=500, message=_subscript(embeddings, "error"))
## PROCESS RESPONSE ## ## PROCESS RESPONSE ##
return self._process_embedding_response( return self._process_embedding_response(
@ -394,10 +419,10 @@ class HuggingFaceEmbedding(BaseLLM):
original_response=response, original_response=response,
) )
embeddings: Final = response.json() embeddings: Final = _DECODED_JSON.validate_python(response.json())
if "error" in embeddings: if json_contains(embeddings, "error"):
raise HuggingFaceError(status_code=500, message=embeddings["error"]) raise HuggingFaceError(status_code=500, message=_subscript(embeddings, "error"))
## PROCESS RESPONSE ## ## PROCESS RESPONSE ##
return self._process_embedding_response( return self._process_embedding_response(

View file

@ -365,7 +365,10 @@ class HuggingFaceEmbeddingConfig(BaseConfig):
) )
content: str = "" content: str = ""
for chunk in streamed_response: for chunk in streamed_response:
content += chunk["choices"][0]["delta"]["content"] delta_content = chunk.choices[0].delta.content
if delta_content is None:
raise TypeError('can only concatenate str (not "NoneType") to str')
content += delta_content
completion_response: Final[list[dict[str, str]]] = [{"generated_text": content}] completion_response: Final[list[dict[str, str]]] = [{"generated_text": content}]
## LOGGING ## LOGGING
logging_obj.post_call( logging_obj.post_call(

View file

@ -15,11 +15,12 @@ import base64
import json import json
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from enum import Enum from enum import Enum
from typing import Any, Final, Protocol, TypedDict from typing import Final, Protocol, TypedDict
from typing_extensions import ReadOnly from typing_extensions import ReadOnly
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
from litellm.litellm_core_utils.core_helpers import require_attributes
class _ToolParameterSchema(TypedDict, total=False): class _ToolParameterSchema(TypedDict, total=False):
@ -237,17 +238,21 @@ class CodeExecutionHandler:
execution_results: Final[list[_ExecutionResult]] = [] execution_results: Final[list[_ExecutionResult]] = []
executor: Final = SkillsSandboxExecutor(timeout=self.sandbox_timeout) executor: Final = SkillsSandboxExecutor(timeout=self.sandbox_timeout)
response: Any = None # Initialize to avoid possibly unbound error response: _CompletionResponse | None = None # Initialize to avoid possibly unbound error
for iteration in range(self.max_iterations): for iteration in range(self.max_iterations):
verbose_logger.debug("CodeExecutionHandler: Iteration %s/%s", iteration + 1, self.max_iterations) verbose_logger.debug("CodeExecutionHandler: Iteration %s/%s", iteration + 1, self.max_iterations)
# Make LLM call # Make LLM call
response = await litellm.acompletion( response = require_attributes(
model=model, await litellm.acompletion(
messages=current_messages, model=model,
tools=tools, messages=current_messages,
**kwargs, tools=tools,
**kwargs,
),
_CompletionResponse,
"choices",
) )
choice: _ResponseChoice = response.choices[0] choice: _ResponseChoice = response.choices[0]

View file

@ -10,8 +10,6 @@ Pattern follows litellm/llms/litellm_proxy/responses/transformation.py
from collections.abc import Coroutine, Sequence from collections.abc import Coroutine, Sequence
from typing import TYPE_CHECKING, Final, Optional from typing import TYPE_CHECKING, Final, Optional
from pydantic import JsonValue
from litellm.types.llms.anthropic_skills import ( from litellm.types.llms.anthropic_skills import (
DeleteSkillResponse, DeleteSkillResponse,
ListSkillsResponse, ListSkillsResponse,
@ -47,7 +45,7 @@ class LiteLLMSkillsTransformationHandler:
file_content: bytes | None = None, file_content: bytes | None = None,
file_name: str | None = None, file_name: str | None = None,
file_type: str | None = None, file_type: str | None = None,
metadata: dict[str, JsonValue] | None = None, metadata: dict[str, object] | None = None,
user_id: str | None = None, user_id: str | None = None,
user_api_key_dict: Optional["UserAPIKeyAuth"] = None, user_api_key_dict: Optional["UserAPIKeyAuth"] = None,
_is_async: bool = False, _is_async: bool = False,
@ -130,7 +128,7 @@ class LiteLLMSkillsTransformationHandler:
file_content: bytes | None = None, file_content: bytes | None = None,
file_name: str | None = None, file_name: str | None = None,
file_type: str | None = None, file_type: str | None = None,
metadata: dict[str, JsonValue] | None = None, metadata: dict[str, object] | None = None,
user_id: str | None = None, user_id: str | None = None,
user_api_key_dict: Optional["UserAPIKeyAuth"] = None, user_api_key_dict: Optional["UserAPIKeyAuth"] = None,
) -> Skill: ) -> Skill:

View file

@ -252,10 +252,11 @@ class MilvusVectorStoreConfig(BaseQueryEmbeddingVectorStoreConfig):
# Build attributes with all available metadata # Build attributes with all available metadata
# Exclude system fields and already-processed fields # Exclude system fields and already-processed fields
attributes = {} attributes = {
for key, value in result.items(): key: value
if key not in ["id", "content", "distance", text_field]: for key, value in decoded_json_object(_DECODED_JSON.validate_python(result)).items()
attributes[key] = value if key not in ["id", "content", "distance", text_field]
}
result_obj = VectorStoreSearchResult( result_obj = VectorStoreSearchResult(
score=score, score=score,

View file

@ -21,6 +21,7 @@ from litellm._logging import verbose_logger
from litellm.exceptions import UnsupportedParamsError from litellm.exceptions import UnsupportedParamsError
from litellm.litellm_core_utils.core_helpers import as_str_mapping from litellm.litellm_core_utils.core_helpers import as_str_mapping
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.litellm_core_utils.llm_cost_calc.guardrail_cost import is_str_keyed_dict
from litellm.litellm_core_utils.prompt_templates.common_utils import ( from litellm.litellm_core_utils.prompt_templates.common_utils import (
convert_content_list_to_str, convert_content_list_to_str,
strip_litellm_internal_message_fields, strip_litellm_internal_message_fields,
@ -82,6 +83,7 @@ _MISTRAL_REQUEST_ROUTING: Final = TypeAdapter(MistralRequestRouting)
_MISTRAL_CACHE_KEY_PARAMS: Final = TypeAdapter(MistralCacheKeyParams) _MISTRAL_CACHE_KEY_PARAMS: Final = TypeAdapter(MistralCacheKeyParams)
_MISTRAL_REASONING_EFFORT_PARAMS: Final = TypeAdapter(MistralReasoningEffortParams) _MISTRAL_REASONING_EFFORT_PARAMS: Final = TypeAdapter(MistralReasoningEffortParams)
_MISTRAL_FILE_PART: Final = TypeAdapter(MistralFilePart) _MISTRAL_FILE_PART: Final = TypeAdapter(MistralFilePart)
_DECODED_JSON: Final = TypeAdapter(object)
_REASONING_PROMPT_REASON: Final = "litellm sends Magistral a reasoning system prompt instead of the requested level." _REASONING_PROMPT_REASON: Final = "litellm sends Magistral a reasoning system prompt instead of the requested level."
_NON_LEVEL_EFFORT_REASON: Final = ( _NON_LEVEL_EFFORT_REASON: Final = (
"the Mistral API takes it only as a level such as 'high', so Mistral applies its default effort." "the Mistral API takes it only as a level such as 'high', so Mistral applies its default effort."
@ -1005,9 +1007,13 @@ class MistralConfig(OpenAIGPTConfig):
logging_obj.model_call_details["response_headers"] = raw_response.headers logging_obj.model_call_details["response_headers"] = raw_response.headers
# Handle Mistral-specific response transformations # Handle Mistral-specific response transformations
response_data = raw_response.json() decoded_response: Final = _DECODED_JSON.validate_python(raw_response.json())
response_data = self._handle_empty_content_response(response_data) if not is_str_keyed_dict(decoded_response):
response_data = self._handle_content_list_to_str_conversion(response_data) missing_get: Final = f"{type(decoded_response).__name__!r} object has no attribute 'get'"
raise AttributeError(missing_get, name="get", obj=decoded_response)
response_data: Final = self._handle_content_list_to_str_conversion(
self._handle_empty_content_response(decoded_response)
)
final_response_obj: Final = cast( final_response_obj: Final = cast(
ModelResponse, ModelResponse,

View file

@ -286,7 +286,7 @@ def handle_cohere_response(
def handle_cohere_stream_chunk( def handle_cohere_stream_chunk(
dict_chunk: Mapping[str, JsonValue], dict_chunk: Mapping[str, object],
prior_tool_calls_emitted: bool = False, prior_tool_calls_emitted: bool = False,
prior_text_emitted: bool = False, prior_text_emitted: bool = False,
) -> ModelResponseStream: ) -> ModelResponseStream:

View file

@ -8,6 +8,7 @@ parsing, and streaming chunk parsing for models served with
import datetime import datetime
import hashlib import hashlib
from collections.abc import Mapping
from typing import Final from typing import Final
import httpx import httpx
@ -363,7 +364,7 @@ def handle_generic_response(
return model_response return model_response
def handle_generic_stream_chunk(dict_chunk: dict) -> ModelResponseStream: def handle_generic_stream_chunk(dict_chunk: Mapping) -> ModelResponseStream:
"""Parse a single GENERIC SSE chunk into a LiteLLM ModelResponseStream.""" """Parse a single GENERIC SSE chunk into a LiteLLM ModelResponseStream."""
# OCI streams tool calls progressively — early chunks may omit required fields. # OCI streams tool calls progressively — early chunks may omit required fields.
if dict_chunk.get("message") and dict_chunk["message"].get("toolCalls"): if dict_chunk.get("message") and dict_chunk["message"].get("toolCalls"):

View file

@ -19,6 +19,7 @@ from pydantic import TypeAdapter, ValidationError
import litellm import litellm
from litellm._logging import verbose_logger from litellm._logging import verbose_logger
from litellm.constants import DEFAULT_OCI_CHAT_MAX_TOKENS from litellm.constants import DEFAULT_OCI_CHAT_MAX_TOKENS
from litellm.litellm_core_utils.core_helpers import decoded_json_object
from litellm.litellm_core_utils.logging_utils import track_llm_api_timing from litellm.litellm_core_utils.logging_utils import track_llm_api_timing
from litellm.litellm_core_utils.prompt_templates.common_utils import ( from litellm.litellm_core_utils.prompt_templates.common_utils import (
merge_consecutive_system_messages, merge_consecutive_system_messages,
@ -891,13 +892,14 @@ class OCIStreamWrapper(CustomStreamWrapper):
if payload == "[DONE]": if payload == "[DONE]":
return None return None
try: try:
dict_chunk: Final = json.loads(payload) decoded_chunk: Final = _DECODED_JSON.validate_python(json.loads(payload))
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
raise OCIError( raise OCIError(
status_code=500, status_code=500,
message=f"Chunk cannot be parsed as JSON: {e}", message=f"Chunk cannot be parsed as JSON: {e}",
) )
dict_chunk: Final = decoded_json_object(decoded_chunk)
if dict_chunk.get("apiFormat") == "COHERE": if dict_chunk.get("apiFormat") == "COHERE":
result: Final = handle_cohere_stream_chunk( result: Final = handle_cohere_stream_chunk(
dict_chunk, dict_chunk,

View file

@ -1,6 +1,7 @@
import base64 import base64
import io import io
from typing import Final, overload from collections.abc import Iterable
from typing import Final, Protocol, overload
import httpx import httpx
from openai.types.chat import ChatCompletionChunk from openai.types.chat import ChatCompletionChunk
@ -8,6 +9,7 @@ from pydantic import BaseModel, TypeAdapter
from litellm import verbose_logger from litellm import verbose_logger
from litellm._uuid import uuid from litellm._uuid import uuid
from litellm.litellm_core_utils.core_helpers import require_attributes
from litellm.litellm_core_utils.reasoning_effort_utils import warn_dropped_reasoning from litellm.litellm_core_utils.reasoning_effort_utils import warn_dropped_reasoning
from litellm.litellm_core_utils.url_utils import join_api_base from litellm.litellm_core_utils.url_utils import join_api_base
from litellm.llms.base_llm.chat.transformation import BaseLLMException from litellm.llms.base_llm.chat.transformation import BaseLLMException
@ -19,6 +21,14 @@ _THINK_OFF_REASON: Final = "Ollama takes think only as on or off, and litellm ma
_DECODED_JSON: Final = TypeAdapter(object) _DECODED_JSON: Final = TypeAdapter(object)
class _JsonLookup(Protocol):
def get(self, key: str, default: object, /) -> object: ...
class _JsonItems(Protocol):
def items(self) -> Iterable[tuple[str, object]]: ...
class OllamaResponseIdNormalizer: class OllamaResponseIdNormalizer:
def __init__(self) -> None: def __init__(self) -> None:
self.response_id: str | None = None self.response_id: str | None = None
@ -206,15 +216,15 @@ class OllamaModelInfo(BaseLLMModelInfo):
return any(name.lower() in model_cost_keys for name in potential_model_names) return any(name.lower() in model_cost_keys for name in potential_model_names)
@staticmethod @staticmethod
def _supports_function_calling(ollama_model_info: dict) -> bool: def _supports_function_calling(ollama_model_info: object) -> bool:
_template: Final[str] = str(ollama_model_info.get("template", "") or "") _template: Final[str] = str(require_attributes(ollama_model_info, _JsonLookup, "get").get("template", "") or "")
return "tools" in _template.lower() return "tools" in _template.lower()
@staticmethod @staticmethod
def _get_max_tokens(ollama_model_info: dict) -> int | None: def _get_max_tokens(ollama_model_info: object) -> object:
_model_info: Final[dict] = ollama_model_info.get("model_info", {}) _model_info: Final = require_attributes(ollama_model_info, _JsonLookup, "get").get("model_info", {})
for key, value in _model_info.items(): for key, value in require_attributes(_model_info, _JsonItems, "items").items():
if "context_length" in key: if "context_length" in key:
return value return value
return None return None
@ -253,7 +263,7 @@ class OllamaModelInfo(BaseLLMModelInfo):
"max_output_tokens": None, "max_output_tokens": None,
} }
model_info: Final = response.json() model_info: Final = _DECODED_JSON.validate_python(response.json())
max_tokens: Final = self._get_max_tokens(model_info) max_tokens: Final = self._get_max_tokens(model_info)
return { return {

View file

@ -6,12 +6,13 @@ Ollama /chat/completion calls handled in llm_http_handler.py
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from types import MappingProxyType from types import MappingProxyType
from typing import Any, Final, Protocol, TypedDict from typing import Final, Protocol, TypedDict
import httpx import httpx
from typing_extensions import NotRequired, ReadOnly from typing_extensions import NotRequired, ReadOnly
import litellm import litellm
from litellm.litellm_core_utils.core_helpers import require_attributes
from litellm.litellm_core_utils.url_utils import ensure_api_route from litellm.litellm_core_utils.url_utils import ensure_api_route
from litellm.types.mcp import has_header from litellm.types.mcp import has_header
from litellm.types.utils import EmbeddingResponse from litellm.types.utils import EmbeddingResponse
@ -23,6 +24,14 @@ class TokenEncoder(Protocol):
def encode(self, text: str, /) -> Sequence[int]: ... def encode(self, text: str, /) -> Sequence[int]: ...
class _DebugLogger(Protocol):
def debug(self, message: str, /) -> object: ...
class _WarningLogger(Protocol):
def warning(self, message: str, /) -> object: ...
class OllamaEmbeddingResponse(TypedDict): class OllamaEmbeddingResponse(TypedDict):
"""Body of an Ollama ``/api/embed`` response.""" """Body of an Ollama ``/api/embed`` response."""
@ -51,7 +60,7 @@ def _process_ollama_embedding_response(
prompts: list[str], prompts: list[str],
model: str, model: str,
model_response: EmbeddingResponse, model_response: EmbeddingResponse,
logging_obj: Any, logging_obj: object,
encoding: TokenEncoder | None, encoding: TokenEncoder | None,
) -> EmbeddingResponse: ) -> EmbeddingResponse:
output_data: Final = [] output_data: Final = []
@ -66,11 +75,15 @@ def _process_ollama_embedding_response(
if encoding is not None: if encoding is not None:
input_tokens = len(encoding.encode("".join(prompts))) input_tokens = len(encoding.encode("".join(prompts)))
if logging_obj: if logging_obj:
logging_obj.debug("Ollama response missing prompt_eval_count; estimated with encoding.") require_attributes(logging_obj, _DebugLogger, "debug").debug(
"Ollama response missing prompt_eval_count; estimated with encoding."
)
else: else:
input_tokens = 0 input_tokens = 0
if logging_obj: if logging_obj:
logging_obj.warning("Missing prompt_eval_count and no encoding provided; defaulted to 0.") require_attributes(logging_obj, _WarningLogger, "warning").warning(
"Missing prompt_eval_count and no encoding provided; defaulted to 0."
)
model_response.object = "list" model_response.object = "list"
model_response.data = output_data model_response.data = output_data

Some files were not shown because too many files have changed in this diff Show more