chore(typing): clear basedpyright Any errors in responses, proxy hooks, and management endpoints

Replaces Any seams with real types across the eight production files carrying the
highest combined reportAny + reportExplicitAny counts. Payload dicts become TypedDicts,
record shapes become Protocols, and db/provider helpers get precise return types, so the
values carry their real shapes instead of laundering them through Any.

Typing only; no runtime behavior changes. No cast(), no type: ignore, and no pyright or
noqa suppressions were added.

reportAny drops 19234 to 18638 and reportExplicitAny 6518 to 6289, with every other rule
at or below its baseline repo-wide. The three lint budgets are ratcheted down to match.
This commit is contained in:
mateo-berri 2026-08-03 03:42:04 +00:00
parent 491eda319c
commit 0db81a60ad
No known key found for this signature in database
16 changed files with 1123 additions and 585 deletions

View file

@ -1,9 +1,9 @@
{
"reportAny": {
"limit": 29813
"limit": 29217
},
"reportArgumentType": {
"limit": 2645
"limit": 2632
},
"reportAssignmentType": {
"limit": 329
@ -24,7 +24,7 @@
"limit": 42
},
"reportExplicitAny": {
"limit": 9473
"limit": 9244
},
"reportFunctionMemberAccess": {
"limit": 11
@ -54,10 +54,10 @@
"limit": 0
},
"reportMissingParameterType": {
"limit": 5855
"limit": 5850
},
"reportMissingTypeArgument": {
"limit": 15852
"limit": 15810
},
"reportMissingTypeStubs": {
"limit": 41
@ -72,7 +72,7 @@
"limit": 0
},
"reportOptionalMemberAccess": {
"limit": 1079
"limit": 1071
},
"reportOptionalOperand": {
"limit": 0
@ -90,7 +90,7 @@
"limit": 12
},
"reportReturnType": {
"limit": 219
"limit": 218
},
"reportTypedDictNotRequiredAccess": {
"limit": 27
@ -99,22 +99,22 @@
"limit": 0
},
"reportUnknownArgumentType": {
"limit": 45324
"limit": 45296
},
"reportUnknownLambdaType": {
"limit": 113
},
"reportUnknownMemberType": {
"limit": 40452
"limit": 40362
},
"reportUnknownParameterType": {
"limit": 20309
"limit": 20268
},
"reportUnknownVariableType": {
"limit": 31978
"limit": 31896
},
"reportUnnecessaryCast": {
"limit": 177
"limit": 173
},
"reportUnnecessaryComparison": {
"limit": 1021
@ -123,7 +123,7 @@
"limit": 7
},
"reportUnnecessaryIsInstance": {
"limit": 1204
"limit": 1203
},
"reportUntypedBaseClass": {
"limit": 165

View file

@ -11,7 +11,6 @@ from litellm.types.utils import (
CacheCreationTokenDetails,
ChatCompletionAudioResponse,
ChatCompletionMessageToolCall,
Choices,
CompletionTokensDetails,
CompletionTokensDetailsWrapper,
Function,
@ -25,7 +24,13 @@ from litellm.types.utils import (
from litellm.utils import print_verbose, token_counter
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.types.litellm_core_utils.streaming_chunk_builder_utils import (
StreamingChunkDict,
StreamingUsageDict,
ToolCallAccumulator,
ToolCallParams,
UsageChunkCalculation,
UsagePerChunk,
)
from litellm.types.llms.openai import (
@ -34,6 +39,25 @@ if TYPE_CHECKING:
)
def _as_str_keyed_dict(value: object) -> dict[str, object] | None:
if isinstance(value, dict):
return value
return None
def _extract_hidden_params(chunk: object) -> dict[str, object]:
chunk_dict = _as_str_keyed_dict(chunk)
params: object
if chunk_dict is not None:
params = chunk_dict.get("_hidden_params", {})
else:
params = getattr(chunk, "_hidden_params", {})
params_dict = _as_str_keyed_dict(params)
if params_dict is not None:
return params_dict
return {}
class ChunkProcessor:
def __init__(self, chunks: list, messages: list | None = None):
self.chunks = self._sort_chunks(chunks)
@ -44,33 +68,19 @@ class ChunkProcessor:
if not chunks:
return []
first_chunk = chunks[0]
first_hidden_params: dict[str, Any] = {}
if isinstance(first_chunk, dict):
candidate = first_chunk.get("_hidden_params", {})
if isinstance(candidate, dict):
first_hidden_params = candidate
else:
candidate = getattr(first_chunk, "_hidden_params", {})
if isinstance(candidate, dict):
first_hidden_params = candidate
first_hidden_params = _extract_hidden_params(chunks[0])
if first_hidden_params.get("created_at"):
def _created_at(chunk: Any) -> int | float:
if isinstance(chunk, dict):
params = chunk.get("_hidden_params", {})
else:
params = getattr(chunk, "_hidden_params", {})
if isinstance(params, dict):
return cast(int | float, params.get("created_at", float("inf")))
return float("inf")
def _created_at(chunk: object) -> int | float:
params = _extract_hidden_params(chunk)
return cast(int | float, params.get("created_at", float("inf")))
return sorted(chunks, key=_created_at)
return chunks
def update_model_response_with_hidden_params(
self, model_response: ModelResponse, chunk: dict[str, Any] | None = None
self, model_response: ModelResponse, chunk: "StreamingChunkDict | None" = None
) -> ModelResponse:
if chunk is None:
return model_response
@ -82,19 +92,20 @@ class ChunkProcessor:
@staticmethod
def apply_provider_assembled_streaming_metadata(
response: ModelResponse,
chunks: list[Any],
logging_obj: Any | None = None,
chunks: list[object],
logging_obj: "LiteLLMLoggingObj | None" = None,
) -> None:
if not chunks:
return
model = getattr(response, "model", None)
model = response.model
if not model:
return
custom_llm_provider = None
custom_llm_provider: object = None
if logging_obj is not None:
custom_llm_provider = logging_obj.model_call_details.get("custom_llm_provider")
model_call_details: dict[str, object] = logging_obj.model_call_details
custom_llm_provider = model_call_details.get("custom_llm_provider")
try:
from litellm.litellm_core_utils.get_llm_provider_logic import (
@ -126,7 +137,7 @@ class ChunkProcessor:
)
@staticmethod
def _get_chunk_id(chunks: list[dict[str, Any]]) -> str:
def _get_chunk_id(chunks: list["StreamingChunkDict"]) -> str:
"""
Chunks:
[{"id": ""}, {"id": "1"}, {"id": "1"}]
@ -137,7 +148,7 @@ class ChunkProcessor:
return ""
@staticmethod
def _get_model_from_chunks(chunks: list[dict[str, Any]], first_chunk_model: str) -> str:
def _get_model_from_chunks(chunks: list["StreamingChunkDict"], first_chunk_model: str) -> str:
"""
Get the actual model from chunks, preferring a model that differs from the first chunk.
@ -153,7 +164,7 @@ class ChunkProcessor:
# Fall back to first chunk's model if no different model found
return first_chunk_model
def build_base_response(self, chunks: list[dict[str, Any]]) -> ModelResponse:
def build_base_response(self, chunks: list["StreamingChunkDict"]) -> ModelResponse:
chunk = self.first_chunk
id = ChunkProcessor._get_chunk_id(chunks)
object = chunk["object"]
@ -168,11 +179,12 @@ class ChunkProcessor:
finish_reason = "stop"
for chunk in chunks:
if "choices" in chunk and len(chunk["choices"]) > 0:
chunk_finish_reason = None
if hasattr(chunk["choices"][0], "finish_reason"):
chunk_finish_reason = chunk["choices"][0].finish_reason
elif "finish_reason" in chunk["choices"][0]:
chunk_finish_reason = chunk["choices"][0]["finish_reason"]
first_choice = chunk["choices"][0]
chunk_finish_reason: str | None = None
if hasattr(first_choice, "finish_reason"):
chunk_finish_reason = first_choice.finish_reason
elif "finish_reason" in first_choice:
chunk_finish_reason = first_choice["finish_reason"]
if chunk_finish_reason is not None:
finish_reason = chunk_finish_reason
@ -202,9 +214,11 @@ class ChunkProcessor:
response = self.update_model_response_with_hidden_params(model_response=response, chunk=chunk)
return response
def get_combined_tool_content(self, tool_call_chunks: list[dict[str, Any]]) -> list[ChatCompletionMessageToolCall]:
def get_combined_tool_content(
self, tool_call_chunks: list["StreamingChunkDict"]
) -> list[ChatCompletionMessageToolCall]:
tool_calls_list: list[ChatCompletionMessageToolCall] = []
tool_call_map: dict[int, dict[str, Any]] = {} # Map to store tool calls by index
tool_call_map: dict[int, ToolCallAccumulator] = {} # Map to store tool calls by index
for chunk in tool_call_chunks:
choices = chunk["choices"]
@ -244,18 +258,22 @@ class ChunkProcessor:
# Extract id, type, and function data (handle both dict and object)
if isinstance(tool_call, dict):
if tool_call.get("id"):
tool_call_map[index]["id"] = tool_call["id"]
if tool_call.get("type"):
tool_call_map[index]["type"] = tool_call["type"]
dict_tool_call_id = tool_call.get("id")
if dict_tool_call_id:
tool_call_map[index]["id"] = dict_tool_call_id
dict_tool_call_type = tool_call.get("type")
if dict_tool_call_type:
tool_call_map[index]["type"] = dict_tool_call_type
function = tool_call.get("function", {})
if isinstance(function, dict):
if function.get("name"):
tool_call_map[index]["name"] = function["name"]
if function.get("arguments"):
tool_call_map[index]["arguments"].append(function["arguments"])
else:
dict_function_name = function.get("name")
if dict_function_name:
tool_call_map[index]["name"] = dict_function_name
dict_function_arguments = function.get("arguments")
if dict_function_arguments:
tool_call_map[index]["arguments"].append(dict_function_arguments)
elif function is not None:
# function is an object
if hasattr(function, "name") and function.name:
tool_call_map[index]["name"] = function.name
@ -267,7 +285,7 @@ class ChunkProcessor:
tool_call_map[index]["id"] = tool_call.id
if hasattr(tool_call, "type") and tool_call.type:
tool_call_map[index]["type"] = tool_call.type
if hasattr(tool_call, "function"):
if hasattr(tool_call, "function") and tool_call.function is not None:
if hasattr(tool_call.function, "name") and tool_call.function.name:
tool_call_map[index]["name"] = tool_call.function.name
if hasattr(tool_call.function, "arguments") and tool_call.function.arguments:
@ -277,13 +295,15 @@ class ChunkProcessor:
provider_fields = None
if isinstance(tool_call, dict):
provider_fields = tool_call.get("provider_specific_fields")
if not provider_fields and isinstance(tool_call.get("function"), dict):
provider_fields = tool_call["function"].get("provider_specific_fields")
dict_tool_call_function = tool_call.get("function")
if not provider_fields and isinstance(dict_tool_call_function, dict):
provider_fields = dict_tool_call_function.get("provider_specific_fields")
else:
if hasattr(tool_call, "provider_specific_fields") and tool_call.provider_specific_fields:
provider_fields = tool_call.provider_specific_fields
elif (
hasattr(tool_call, "function")
and tool_call.function is not None
and hasattr(tool_call.function, "provider_specific_fields")
and tool_call.function.provider_specific_fields
):
@ -291,10 +311,12 @@ class ChunkProcessor:
if provider_fields:
# Merge provider_specific_fields if multiple chunks have them
if tool_call_map[index]["provider_specific_fields"] is None:
tool_call_map[index]["provider_specific_fields"] = {}
merged_provider_fields = tool_call_map[index]["provider_specific_fields"]
if merged_provider_fields is None:
merged_provider_fields = {}
tool_call_map[index]["provider_specific_fields"] = merged_provider_fields
if isinstance(provider_fields, dict):
tool_call_map[index]["provider_specific_fields"].update(provider_fields)
merged_provider_fields.update(provider_fields)
# Convert the map to a list of tool calls
for index in sorted(tool_call_map.keys()):
@ -309,15 +331,16 @@ class ChunkProcessor:
)
# Prepare params for ChatCompletionMessageToolCall
tool_call_params = {
tool_call_params: ToolCallParams = {
"id": tool_call_data["id"],
"function": function,
"type": tool_call_data["type"] or "function",
}
# Add provider_specific_fields if present (for thought signatures in Gemini 3)
if tool_call_data.get("provider_specific_fields"):
tool_call_params["provider_specific_fields"] = tool_call_data["provider_specific_fields"]
accumulated_provider_fields = tool_call_data["provider_specific_fields"]
if accumulated_provider_fields:
tool_call_params["provider_specific_fields"] = accumulated_provider_fields
tool_call = ChatCompletionMessageToolCall(**tool_call_params)
tool_calls_list.append(tool_call)
@ -350,7 +373,7 @@ class ChunkProcessor:
)
def get_combined_content(
self, chunks: list[dict[str, Any]], delta_key: str = "content"
self, chunks: list["StreamingChunkDict"], delta_key: str = "content"
) -> ChatCompletionAssistantContentValue:
content_list: list[str] = []
for chunk in chunks:
@ -369,7 +392,7 @@ class ChunkProcessor:
return combined_content
def get_combined_thinking_content(
self, chunks: list[dict[str, Any]]
self, chunks: list["StreamingChunkDict"]
) -> list[Union["ChatCompletionThinkingBlock", "ChatCompletionRedactedThinkingBlock"]] | None:
from litellm.types.llms.openai import (
ChatCompletionRedactedThinkingBlock,
@ -426,10 +449,10 @@ class ChunkProcessor:
return thinking_blocks
return None
def get_combined_reasoning_content(self, chunks: list[dict[str, Any]]) -> ChatCompletionAssistantContentValue:
def get_combined_reasoning_content(self, chunks: list["StreamingChunkDict"]) -> ChatCompletionAssistantContentValue:
return self.get_combined_content(chunks, delta_key="reasoning_content")
def get_combined_audio_content(self, chunks: list[dict[str, Any]]) -> ChatCompletionAudioResponse:
def get_combined_audio_content(self, chunks: list["StreamingChunkDict"]) -> ChatCompletionAudioResponse:
base64_data_list: list[str] = []
transcript_list: list[str] = []
expires_at: int | None = None
@ -459,9 +482,9 @@ class ChunkProcessor:
id=id,
)
def _usage_chunk_calculation_helper(self, usage_chunk: Usage) -> dict:
prompt_tokens = 0
completion_tokens = 0
def _usage_chunk_calculation_helper(self, usage_chunk: Usage) -> "UsageChunkCalculation":
prompt_tokens: int = 0
completion_tokens: int = 0
## anthropic prompt caching information ##
cache_creation_input_tokens: int | None = None
cache_read_input_tokens: int | None = None
@ -503,22 +526,19 @@ class ChunkProcessor:
def count_reasoning_tokens(self, response: ModelResponse) -> int | None:
reasoning_tokens: int | None = None
for choice in response.choices:
if (
hasattr(cast(Choices, choice).message, "reasoning_content")
and cast(Choices, choice).message.reasoning_content is not None
):
if hasattr(choice.message, "reasoning_content") and choice.message.reasoning_content is not None:
if reasoning_tokens is None:
reasoning_tokens = 0
reasoning_tokens += token_counter(
text=cast(Choices, choice).message.reasoning_content,
text=choice.message.reasoning_content,
count_response_tokens=True,
)
return reasoning_tokens
@staticmethod
def _extract_usage_chunk(chunk: dict[str, Any] | ModelResponse | ModelResponseStream) -> Usage | None:
usage_chunk: Usage | dict[str, Any] | None = None
def _extract_usage_chunk(chunk: "StreamingChunkDict | ModelResponse | ModelResponseStream") -> Usage | None:
usage_chunk: Usage | StreamingUsageDict | None = None
if hasattr(chunk, "usage") and chunk.usage is not None:
usage_chunk = chunk.usage
elif "usage" in chunk:
@ -534,7 +554,7 @@ class ChunkProcessor:
def _calculate_usage_per_chunk(
self,
chunks: list[dict[str, Any] | ModelResponse],
chunks: list["StreamingChunkDict | ModelResponse"],
) -> "UsagePerChunk":
from litellm.types.litellm_core_utils.streaming_chunk_builder_utils import (
UsagePerChunk,
@ -615,10 +635,7 @@ class ChunkProcessor:
"web_search_requests",
)
prompt_tokens_details = cast(
PromptTokensDetailsWrapper | None,
usage_chunk_dict["prompt_tokens_details"],
)
prompt_tokens_details = usage_chunk_dict["prompt_tokens_details"]
cache_creation_token_details = self._capture_cache_creation_token_details(
prompt_tokens_details, cache_creation_token_details
@ -679,7 +696,7 @@ class ChunkProcessor:
@staticmethod
def _reset_anthropic_cursor_completion_tokens(
chunks: list[dict[str, Any] | ModelResponse],
chunks: list["StreamingChunkDict | ModelResponse"],
completion_tokens: int,
completion_usage_updates: int,
) -> int:
@ -702,7 +719,7 @@ class ChunkProcessor:
if saw_non_cursor_completion:
return completion_tokens
custom_llm_provider: str | None = None
custom_llm_provider: object = None
if chunks:
first_chunk = chunks[0]
if isinstance(first_chunk, dict):
@ -718,7 +735,7 @@ class ChunkProcessor:
def calculate_usage(
self,
chunks: list[dict[str, Any] | ModelResponse],
chunks: list["StreamingChunkDict | ModelResponse"],
model: str,
completion_output: str,
messages: list | None = None,

View file

@ -1,6 +1,7 @@
from collections.abc import AsyncIterator, Coroutine, Iterator
from typing import (
Any,
TypeAlias,
cast,
)
@ -27,8 +28,11 @@ from litellm.utils import get_model_info
# Anthropic-only keys already mapped by the translator; strip on extra_kwargs re-merge.
ANTHROPIC_ONLY_REQUEST_KEYS: frozenset[str] = frozenset({"output_config"})
ContextManagementSpec: TypeAlias = dict[str, object] | list[dict[str, object]] | None
AnthropicSystemPrompt: TypeAlias = str | list[dict[str, object]] | None
def _messages_have_compaction_block(messages: list[dict]) -> bool:
def _messages_have_compaction_block(messages: list[dict[str, object]]) -> bool:
"""Return True when any message carries a ``compaction`` content block."""
for msg in messages:
content = msg.get("content")
@ -40,7 +44,7 @@ def _messages_have_compaction_block(messages: list[dict]) -> bool:
return False
def _extract_proxy_litellm_metadata(kwargs: dict[str, Any]) -> dict[str, Any] | None:
def _extract_proxy_litellm_metadata(kwargs: dict[str, object]) -> dict[str, object] | None:
"""Return ``kwargs["litellm_metadata"]`` when it's a dict; ``None`` otherwise.
The proxy attaches its auth/spend-attribution fields (``user_api_key``,
@ -61,14 +65,14 @@ def _extract_proxy_litellm_metadata(kwargs: dict[str, Any]) -> dict[str, Any] |
async def _prepare_context_managed_request(
*,
model: str,
messages: list[dict],
tools: list[dict] | None,
system: Any | None,
context_management_spec: Any,
litellm_metadata: dict | None,
messages: list[dict[str, object]],
tools: list[dict[str, object]] | None,
system: AnthropicSystemPrompt,
context_management_spec: ContextManagementSpec,
litellm_metadata: dict[str, object] | None,
additional_drop_params: list[str] | None,
llm_router: Any,
user_api_key_auth: Any = None,
llm_router: object,
user_api_key_auth: object = None,
) -> PolyfillResult | None:
"""Apply client compaction history, then optional context_management polyfill."""
from litellm.llms.anthropic.experimental_pass_through.context_management.editors.compact import (
@ -88,11 +92,11 @@ async def _prepare_context_managed_request(
if polyfill_will_run:
history_result: PolyfillResult | None = None
working_messages: list[dict] = messages
working_system: Any | None = system
working_messages: list[dict[str, object]] = messages
working_system: AnthropicSystemPrompt = system
else:
history_result = apply_client_compaction_block_history(
messages=cast(list[dict[str, Any]], messages),
messages=messages,
system=system,
)
working_messages = history_result.messages if history_result is not None else messages
@ -122,7 +126,7 @@ async def _prepare_context_managed_request(
# to non-Anthropic backends that would reject them.
if polyfill_will_run and history_result is None:
history_result = apply_client_compaction_block_history(
messages=cast(list[dict[str, Any]], messages),
messages=messages,
system=system,
)
return history_result
@ -130,7 +134,7 @@ async def _prepare_context_managed_request(
def _polyfill_will_run(
*,
context_management_spec: Any,
context_management_spec: ContextManagementSpec,
additional_drop_params: list[str] | None,
) -> bool:
"""Return True when ``compact_20260112`` will run via the polyfill dispatcher.
@ -157,7 +161,7 @@ def _polyfill_will_run(
def _spec_has_non_compact_edits(
*,
context_management_spec: Any,
context_management_spec: ContextManagementSpec,
additional_drop_params: list[str] | None,
) -> bool:
"""Return True when the spec includes edits other than ``compact_20260112``.
@ -198,9 +202,9 @@ def _context_management_explicitly_dropped(additional_drop_params: list[str] | N
def _normalize_spec_edits(
*,
context_management_spec: Any,
context_management_spec: ContextManagementSpec,
additional_drop_params: list[str] | None,
) -> list[dict[str, Any]] | None:
) -> list[dict[str, object]] | None:
"""Return the normalized ``edits`` list, or ``None`` if the polyfill won't run.
Delegates spec-shape normalization to the dispatcher's ``_normalize_spec``
@ -225,14 +229,14 @@ def _normalize_spec_edits(
async def _run_polyfill_if_enabled(
*,
model: str,
messages: list[dict],
tools: list[dict] | None,
system: Any | None,
context_management_spec: Any,
litellm_metadata: dict | None,
messages: list[dict[str, object]],
tools: list[dict[str, object]] | None,
system: AnthropicSystemPrompt,
context_management_spec: ContextManagementSpec,
litellm_metadata: dict[str, object] | None,
additional_drop_params: list[str] | None,
llm_router: Any,
user_api_key_auth: Any = None,
llm_router: object,
user_api_key_auth: object = None,
) -> PolyfillResult | None:
"""Run the async context_management polyfill if a spec is present.
@ -293,9 +297,9 @@ ANTHROPIC_ADAPTER = AnthropicAdapter()
class LiteLLMMessagesToCompletionTransformationHandler:
@staticmethod
def _route_openai_thinking_to_responses_api_if_needed(
completion_kwargs: dict[str, Any],
completion_kwargs: dict[str, object],
*,
thinking: dict[str, Any] | None,
thinking: dict[str, object] | None,
) -> None:
"""
When users call `litellm.anthropic.messages.*` with a non-Anthropic model and
@ -318,7 +322,7 @@ class LiteLLMMessagesToCompletionTransformationHandler:
except Exception:
custom_llm_provider = None
if custom_llm_provider != "openai":
if not isinstance(custom_llm_provider, str) or custom_llm_provider != "openai":
return
if not isinstance(thinking, dict) or thinking.get("type") != "enabled":
@ -342,7 +346,7 @@ class LiteLLMMessagesToCompletionTransformationHandler:
reasoning_effort = completion_kwargs.get("reasoning_effort")
summary = thinking.get("summary")
if isinstance(reasoning_effort, str) and reasoning_effort:
reasoning_dict: dict[str, Any] = {"effort": reasoning_effort}
reasoning_dict: dict[str, object] = {"effort": reasoning_effort}
if summary:
reasoning_dict["summary"] = summary
elif auto_summary:
@ -396,20 +400,20 @@ class LiteLLMMessagesToCompletionTransformationHandler:
def _prepare_completion_kwargs(
*,
max_tokens: int,
messages: list[dict],
messages: list[dict[str, object]],
model: str,
metadata: dict | None = None,
metadata: dict[str, object] | None = None,
stop_sequences: list[str] | None = None,
stream: bool | None = False,
system: str | list[dict[str, Any]] | None = None,
system: AnthropicSystemPrompt = None,
temperature: float | None = None,
thinking: dict | None = None,
tool_choice: dict | None = None,
tools: list[dict] | None = None,
thinking: dict[str, object] | None = None,
tool_choice: dict[str, object] | None = None,
tools: list[dict[str, object]] | None = None,
top_k: int | None = None,
top_p: float | None = None,
output_format: dict | None = None,
extra_kwargs: dict[str, Any] | None = None,
output_format: dict[str, object] | None = None,
extra_kwargs: dict[str, object] | None = None,
) -> tuple[dict[str, Any], dict[str, str]]:
"""Prepare kwargs for litellm.completion/acompletion.
@ -422,7 +426,7 @@ class LiteLLMMessagesToCompletionTransformationHandler:
Logging as LiteLLMLoggingObject,
)
request_data = {
request_data: dict[str, object] = {
"model": model,
"messages": messages,
"max_tokens": max_tokens,
@ -467,7 +471,7 @@ class LiteLLMMessagesToCompletionTransformationHandler:
if openai_request is None:
raise ValueError("Failed to translate request to OpenAI format")
completion_kwargs: dict[str, Any] = dict(openai_request)
completion_kwargs: dict[str, object] = dict(openai_request)
if stream:
completion_kwargs["stream"] = stream
@ -517,21 +521,21 @@ class LiteLLMMessagesToCompletionTransformationHandler:
@staticmethod
async def async_anthropic_messages_handler(
max_tokens: int,
messages: list[dict],
messages: list[dict[str, object]],
model: str,
metadata: dict | None = None,
metadata: dict[str, object] | None = None,
stop_sequences: list[str] | None = None,
stream: bool | None = False,
system: str | None = None,
temperature: float | None = None,
thinking: dict | None = None,
tool_choice: dict | None = None,
tools: list[dict] | None = None,
thinking: dict[str, object] | None = None,
tool_choice: dict[str, object] | None = None,
tools: list[dict[str, object]] | None = None,
top_k: int | None = None,
top_p: float | None = None,
output_format: dict | None = None,
output_format: dict[str, object] | None = None,
**kwargs,
) -> AnthropicMessagesResponse | AsyncIterator[Any] | Iterator[bytes]:
) -> AnthropicMessagesResponse | AsyncIterator[bytes] | Iterator[bytes]:
"""Handle non-Anthropic models asynchronously using the adapter"""
context_management = kwargs.pop("context_management", None)
additional_drop_params: list[str] | None = kwargs.get("additional_drop_params", None)
@ -611,26 +615,26 @@ class LiteLLMMessagesToCompletionTransformationHandler:
@staticmethod
def anthropic_messages_handler(
max_tokens: int,
messages: list[dict],
messages: list[dict[str, object]],
model: str,
metadata: dict | None = None,
metadata: dict[str, object] | None = None,
stop_sequences: list[str] | None = None,
stream: bool | None = False,
system: str | None = None,
temperature: float | None = None,
thinking: dict | None = None,
tool_choice: dict | None = None,
tools: list[dict] | None = None,
thinking: dict[str, object] | None = None,
tool_choice: dict[str, object] | None = None,
tools: list[dict[str, object]] | None = None,
top_k: int | None = None,
top_p: float | None = None,
output_format: dict | None = None,
output_format: dict[str, object] | None = None,
_is_async: bool = False,
**kwargs,
) -> (
AnthropicMessagesResponse
| Iterator[bytes]
| AsyncIterator[Any]
| Coroutine[Any, Any, AnthropicMessagesResponse | AsyncIterator[Any] | Iterator[bytes]]
| AsyncIterator[bytes]
| Coroutine[None, None, AnthropicMessagesResponse | AsyncIterator[bytes] | Iterator[bytes]]
):
"""Handle non-Anthropic models using the adapter."""
if _is_async is True:

View file

@ -16,7 +16,7 @@ from litellm.types.llms.base import LiteLLMPydanticObjectBase
class LiteLLM_ProxyModelTable(LiteLLMPydanticObjectBase):
model_id: str
model_name: str
litellm_params: dict
litellm_params: dict[str, object]
model_info: dict | None = None
blocked: bool = False
created_at: datetime | None = None

View file

@ -295,8 +295,12 @@ def _convert_mcp_content_to_openai(
return _convert_single_content(content)
def _required_attr(obj: object, name: str) -> object:
return getattr(obj, name)
def _convert_single_content(
content: Any,
content: object,
) -> "dict[str, object] | list[dict[str, object]]":
"""Convert a single MCP content item to OpenAI format.
@ -310,7 +314,7 @@ def _convert_single_content(
content_type = getattr(content, "type", None)
if content_type == "text":
return {"type": "text", "text": content.text}
return {"type": "text", "text": _required_attr(content, "text")}
elif content_type == "image":
data = getattr(content, "data", "")
mime_type = getattr(content, "mimeType", "image/png")
@ -581,12 +585,28 @@ def _convert_mcp_tool_choice_to_openai(
return "auto"
class _SamplingToolCallFunction(Protocol):
@property
def name(self) -> str: ...
@property
def arguments(self) -> str | dict[str, object]: ...
class _SamplingToolCall(Protocol):
@property
def id(self) -> str: ...
@property
def function(self) -> _SamplingToolCallFunction: ...
class _SamplingResponseMessage(Protocol):
@property
def content(self) -> str | None: ...
@property
def tool_calls(self) -> Sequence[object] | None: ...
def tool_calls(self) -> Sequence[_SamplingToolCall] | None: ...
class _SamplingResponseChoice(Protocol):
@ -641,7 +661,7 @@ def _convert_openai_response_to_mcp_result(
stop_reason = "endTurn"
actual_model: str = getattr(response, "model", model_name) or model_name
# Check if response has tool calls
tool_calls = getattr(message, "tool_calls", None)
tool_calls: Sequence[_SamplingToolCall] | None = getattr(message, "tool_calls", None)
if tool_calls:
# Build ToolUseContent items
content_parts: list[SamplingMessageContentBlock] = []

View file

@ -8,7 +8,7 @@ import asyncio
import binascii
import os
import uuid
from collections.abc import Callable
from collections.abc import Awaitable, Callable, Sequence
from contextvars import ContextVar
from dataclasses import dataclass, field
from datetime import datetime
@ -16,9 +16,10 @@ from typing import (
TYPE_CHECKING,
Any,
Literal,
Protocol,
TypeAlias,
TypedDict,
Union,
cast,
)
from litellm import DualCache
@ -44,6 +45,7 @@ from litellm.types.caching import RedisPipelineIncrementOperation
from litellm.types.llms.openai import BaseLiteLLMOpenAIResponseObject
from litellm.types.utils import (
CallTypes,
CallTypesLiteral,
EmbeddingResponse,
ModelResponse,
TextCompletionResponse,
@ -54,6 +56,7 @@ if TYPE_CHECKING:
from opentelemetry.trace import Span as _Span
from litellm.proxy.utils import InternalUsageCache as _InternalUsageCache
from litellm.types.agents import AgentResponse
from litellm.types.caching import RedisPipelineIncrementOperation
Span = Union[_Span, Any]
@ -300,6 +303,15 @@ _TPM_FLOOR_FRACTION = 4
PARALLEL_REQUEST_SLOT_TTL_SECONDS = 3600
LuaReplyValue: TypeAlias = int | str | bytes
CachedCounterValue: TypeAlias = int | str | bytes | None
CachedGaugeValue: TypeAlias = dict[str, object] | int | str | bytes | None
class AsyncLuaScript(Protocol):
def __call__(self, *, keys: Sequence[str], args: Sequence[float | str]) -> Awaitable[list[LuaReplyValue]]: ...
class RateLimitDescriptorRateLimitObject(TypedDict, total=False):
requests_per_unit: int | None
tokens_per_unit: int | None
@ -342,6 +354,29 @@ class RateLimitResponseWithDescriptors(TypedDict):
response: RateLimitResponse
class WindowedKeyMetadata(TypedDict):
requests_limit: int | None
tokens_limit: int | None
window_size: int
descriptor_key: str
class AtomicCounterMeta(TypedDict):
descriptor_key: str
current_limit: int
rate_limit_type: Literal["requests", "tokens"]
window_key: str
counter_key: str
increment: int
ttl: int
window_size: int
class AtomicCounterState(TypedDict):
window_expired: bool
current: int
@dataclass(slots=True)
class RequestRateLimiterStash:
"""
@ -423,25 +458,24 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
):
self.internal_usage_cache = internal_usage_cache
self._time_provider = time_provider or datetime.now
if self.internal_usage_cache.dual_cache.redis_cache is not None:
self.batch_rate_limiter_script = self.internal_usage_cache.dual_cache.redis_cache.async_register_script(
redis_cache = self.internal_usage_cache.dual_cache.redis_cache
if redis_cache is not None:
self.batch_rate_limiter_script: AsyncLuaScript | None = redis_cache.async_register_script(
BATCH_RATE_LIMITER_SCRIPT
)
self.token_increment_script = self.internal_usage_cache.dual_cache.redis_cache.async_register_script(
self.token_increment_script: AsyncLuaScript | None = redis_cache.async_register_script(
TOKEN_INCREMENT_SCRIPT
)
self.check_and_increment_by_n_script = (
self.internal_usage_cache.dual_cache.redis_cache.async_register_script(CHECK_AND_INCREMENT_BY_N_SCRIPT)
self.check_and_increment_by_n_script: AsyncLuaScript | None = redis_cache.async_register_script(
CHECK_AND_INCREMENT_BY_N_SCRIPT
)
self.parallel_acquire_script = self.internal_usage_cache.dual_cache.redis_cache.async_register_script(
self.parallel_acquire_script: AsyncLuaScript | None = redis_cache.async_register_script(
PARALLEL_ACQUIRE_SCRIPT
)
self.parallel_release_script = self.internal_usage_cache.dual_cache.redis_cache.async_register_script(
self.parallel_release_script: AsyncLuaScript | None = redis_cache.async_register_script(
PARALLEL_RELEASE_SCRIPT
)
self.parallel_count_script = self.internal_usage_cache.dual_cache.redis_cache.async_register_script(
PARALLEL_COUNT_SCRIPT
)
self.parallel_count_script: AsyncLuaScript | None = redis_cache.async_register_script(PARALLEL_COUNT_SCRIPT)
else:
self.batch_rate_limiter_script = None
self.token_increment_script = None
@ -459,7 +493,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
self.tpm_reservation_enabled = os.getenv("LITELLM_TPM_TOKEN_RESERVATION_ENABLED", "true").lower() == "true"
# Batch rate limiter (lazy loaded)
self._batch_rate_limiter: Any | None = None
self._batch_rate_limiter: CustomLogger | None = None
# Serializes multi-phase check+increment sequences (batch + dynamic
# limiters) within this process to close the TOCTOU window between
@ -477,7 +511,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
# one round-trip.
self._check_and_increment_lock = asyncio.Lock()
def _get_batch_rate_limiter(self) -> Any | None:
def _get_batch_rate_limiter(self) -> CustomLogger | None:
"""Get or lazy-load the batch rate limiter."""
if self._batch_rate_limiter is None:
try:
@ -604,12 +638,12 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
keys: list[str],
now_int: int,
window_size: int,
) -> list[Any]:
) -> list[CachedCounterValue]:
"""
Implement sliding window rate limiting logic using in-memory cache operations.
This follows the same logic as the Redis Lua script but uses async cache operations.
"""
results: list[Any] = []
results: list[CachedCounterValue] = []
# Process each window/counter pair
for i in range(0, len(keys), 2):
@ -618,7 +652,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
increment_value = 1
# Get the window start time
window_start = await self.internal_usage_cache.async_get_cache(
window_start: CachedCounterValue = await self.internal_usage_cache.async_get_cache(
key=window_key,
litellm_parent_otel_span=None,
local_only=True,
@ -645,7 +679,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
results.append(increment_value) # counter
else:
# Increment the counter
current_counter = await self.internal_usage_cache.async_get_cache(
current_counter: CachedCounterValue = await self.internal_usage_cache.async_get_cache(
key=counter_key,
litellm_parent_otel_span=None,
local_only=True,
@ -679,8 +713,8 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
def is_cache_list_over_limit(
self,
keys_to_fetch: list[str],
cache_values: list[Any],
key_metadata: dict[str, Any],
cache_values: Sequence[CachedCounterValue],
key_metadata: dict[str, WindowedKeyMetadata],
) -> RateLimitResponse:
"""
Check if the cache values are over the limit.
@ -783,7 +817,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
self,
keys_to_fetch: list[str],
now_int: int,
) -> list[Any]:
) -> list[CachedCounterValue]:
"""
Execute Redis operations grouped by hash tag for cluster compatibility.
@ -792,13 +826,13 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
now_int: int - Current timestamp
Returns:
List[Any] - List of cache values
list[CachedCounterValue] - List of cache values
"""
if self.batch_rate_limiter_script is None:
return []
key_groups = self._group_keys_by_hash_tag(keys_to_fetch)
all_cache_values = []
all_cache_values: list[CachedCounterValue] = []
for hash_tag, group_keys in key_groups.items():
try:
@ -866,7 +900,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
windowed_response = RateLimitResponse(overall_code="OK", statuses=[])
if keys_to_fetch:
## CHECK IN-MEMORY CACHE
cache_values = await self.internal_usage_cache.async_batch_get_cache(
cache_values: Sequence[CachedCounterValue] | None = await self.internal_usage_cache.async_batch_get_cache(
keys=keys_to_fetch,
parent_otel_span=parent_otel_span,
local_only=True,
@ -888,9 +922,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
# For keys that don't exist yet, set them to 0
if cache_values is None:
cache_values = []
for _ in keys_to_fetch:
cache_values.append(str(now_int) if _.endswith(":window") else 0)
cache_values = [str(now_int) if key.endswith(":window") else 0 for key in keys_to_fetch]
elif self.batch_rate_limiter_script is not None:
# NORMAL MODE: Increment counters in Redis
# Group keys by hash tag for Redis cluster compatibility
@ -949,14 +981,14 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
self,
descriptors: list[RateLimitDescriptor],
skip_tpm_check: bool,
) -> tuple[list[str], dict[str, dict[str, Any]], list[ParallelRequestGauge]]:
) -> tuple[list[str], dict[str, WindowedKeyMetadata], list[ParallelRequestGauge]]:
"""
Split descriptors into the windowed (window_key, counter_key) fetch
list with its per-window metadata, and the concurrency gauges for
descriptors carrying a max_parallel_requests limit.
"""
keys_to_fetch: list[str] = []
key_metadata: dict[str, dict[str, Any]] = {}
key_metadata: dict[str, WindowedKeyMetadata] = {}
gauges: list[ParallelRequestGauge] = []
for descriptor in descriptors:
descriptor_key = descriptor["key"]
@ -995,12 +1027,12 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
if not rate_limit_set:
continue
key_metadata[window_key] = {
"requests_limit": (int(requests_limit) if requests_limit is not None else None),
"tokens_limit": int(tokens_limit) if tokens_limit is not None else None,
"window_size": int(window_size),
"descriptor_key": descriptor_key,
}
key_metadata[window_key] = WindowedKeyMetadata(
requests_limit=(int(requests_limit) if requests_limit is not None else None),
tokens_limit=int(tokens_limit) if tokens_limit is not None else None,
window_size=int(window_size),
descriptor_key=descriptor_key,
)
return keys_to_fetch, key_metadata, gauges
def _gauge_status(self, gauge: ParallelRequestGauge, in_flight: int, code: str) -> RateLimitStatus:
@ -1012,7 +1044,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
descriptor_key=gauge["descriptor_key"],
)
def _gauge_in_flight_from_cache_value(self, raw_value: Any) -> int:
def _gauge_in_flight_from_cache_value(self, raw_value: CachedGaugeValue) -> int:
"""
In-flight count from a cached gauge value: a dict of slot_id ->
acquire timestamp when the in-memory registry is authoritative, or
@ -1114,7 +1146,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
gauge_keys: list[str],
parent_otel_span: Span | None = None,
) -> list[int]:
values = await self.internal_usage_cache.async_batch_get_cache(
values: Sequence[CachedGaugeValue] | None = await self.internal_usage_cache.async_batch_get_cache(
keys=gauge_keys,
parent_otel_span=parent_otel_span,
local_only=True,
@ -1143,7 +1175,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
cutoff = now - PARALLEL_REQUEST_SLOT_TTL_SECONDS
states: list[tuple[dict[str, float] | None, int]] = []
for gauge in gauges:
raw_value = await self.internal_usage_cache.async_get_cache(
raw_value: CachedGaugeValue = await self.internal_usage_cache.async_get_cache(
key=gauge["counter_key"],
litellm_parent_otel_span=parent_otel_span,
local_only=True,
@ -1218,7 +1250,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
async with self._check_and_increment_lock:
for counter_key in counter_keys:
raw_value = await self.internal_usage_cache.async_get_cache(
raw_value: CachedGaugeValue = await self.internal_usage_cache.async_get_cache(
key=counter_key,
litellm_parent_otel_span=parent_otel_span,
local_only=True,
@ -1226,7 +1258,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
if isinstance(raw_value, dict):
if slot_id not in raw_value:
continue
new_value: dict[str, float] | int = {key: ts for key, ts in raw_value.items() if key != slot_id}
new_value: dict[str, object] | int = {key: ts for key, ts in raw_value.items() if key != slot_id}
elif raw_value is None:
continue
else:
@ -1277,7 +1309,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
# Build per-descriptor (keys, args, meta) groups. All keys within a
# group share the descriptor's {key:value} hash tag, so a single Lua
# call per group never triggers CROSSSLOT on Redis Cluster.
descriptor_groups: list[tuple[list[str], list[Any], list[dict[str, Any]]]] = []
descriptor_groups: list[tuple[list[str], list[int], list[AtomicCounterMeta]]] = []
for descriptor, increment_amounts in zip(descriptors, increments):
keys, args, meta = self._build_descriptor_atomic_payload(
descriptor=descriptor,
@ -1300,7 +1332,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
parent_otel_span=parent_otel_span,
)
flat_meta: list[dict[str, Any]] = [m for _keys, _args, group_meta in descriptor_groups for m in group_meta]
flat_meta: list[AtomicCounterMeta] = [m for _keys, _args, group_meta in descriptor_groups for m in group_meta]
async with self._check_and_increment_lock:
return await self._atomic_check_and_increment_in_memory(
per_counter_meta=flat_meta,
@ -1311,7 +1343,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
self,
descriptor: RateLimitDescriptor,
increment_amounts: dict[Literal["requests", "tokens"], int],
) -> tuple[list[str], list[Any], list[dict[str, Any]]]:
) -> tuple[list[str], list[int], list[AtomicCounterMeta]]:
"""
Build (KEYS, ARGV, per-counter meta) for a single descriptor's Lua
call. All keys returned share the descriptor's {key:value} hash tag.
@ -1325,11 +1357,11 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
window_key = f"{{{descriptor_key}:{descriptor_value}}}:window"
keys: list[str] = []
args: list[Any] = []
meta: list[dict[str, Any]] = []
args: list[int] = []
meta: list[AtomicCounterMeta] = []
for rate_limit_type in ("requests", "tokens"):
rlt: Literal["requests", "tokens"] = cast(Literal["requests", "tokens"], rate_limit_type)
rate_limit_types: tuple[Literal["requests", "tokens"], ...] = ("requests", "tokens")
for rlt in rate_limit_types:
if rlt == "requests":
limit_value = rate_limit.get("requests_per_unit")
inc_amount = int(increment_amounts.get("requests", 0) or 0)
@ -1350,22 +1382,22 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
# [limit, increment, ttl_seconds, window_size_seconds].
args.extend([int(limit_value), inc_amount, ttl_seconds, window_size_seconds])
meta.append(
{
"descriptor_key": descriptor_key,
"current_limit": int(limit_value),
"rate_limit_type": rlt,
"window_key": window_key,
"counter_key": counter_key,
"increment": inc_amount,
"ttl": ttl_seconds,
"window_size": window_size_seconds,
}
AtomicCounterMeta(
descriptor_key=descriptor_key,
current_limit=int(limit_value),
rate_limit_type=rlt,
window_key=window_key,
counter_key=counter_key,
increment=inc_amount,
ttl=ttl_seconds,
window_size=window_size_seconds,
)
)
return keys, args, meta
async def _atomic_lua_per_descriptor(
self,
descriptor_groups: list[tuple[list[str], list[Any], list[dict[str, Any]]]],
descriptor_groups: list[tuple[list[str], list[int], list[AtomicCounterMeta]]],
parent_otel_span: Span | None = None,
) -> RateLimitResponse:
"""
@ -1374,7 +1406,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
descriptor i, refund descriptors 0..i-1's increments. On Lua failure
mid-loop, refund applied increments and fall back to in-memory.
"""
applied: list[list[dict[str, Any]]] = []
applied: list[list[AtomicCounterMeta]] = []
statuses: list[RateLimitStatus] = []
for _idx, (keys, args, meta) in enumerate(descriptor_groups):
@ -1397,7 +1429,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
f"{self.window_size}s)."
)
await self._refund_applied_descriptor_groups(applied)
flat_meta: list[dict[str, Any]] = [m for _k, _a, group_meta in descriptor_groups for m in group_meta]
flat_meta: list[AtomicCounterMeta] = [m for _k, _a, group_meta in descriptor_groups for m in group_meta]
async with self._check_and_increment_lock:
return await self._atomic_check_and_increment_in_memory(
per_counter_meta=flat_meta,
@ -1415,7 +1447,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
async def _refund_applied_descriptor_groups(
self,
applied: list[list[dict[str, Any]]],
applied: list[list[AtomicCounterMeta]],
) -> None:
"""
Decrement counters for descriptor groups already applied via Lua.
@ -1441,8 +1473,8 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
def _build_atomic_response(
self,
raw: list[Any],
per_counter_meta: list[dict[str, Any]],
raw: list[LuaReplyValue],
per_counter_meta: list[AtomicCounterMeta],
) -> RateLimitResponse:
"""Convert Lua script return value to RateLimitResponse.
@ -1493,7 +1525,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
async def _atomic_check_and_increment_in_memory(
self,
per_counter_meta: list[dict[str, Any]],
per_counter_meta: list[AtomicCounterMeta],
parent_otel_span: Span | None = None,
) -> RateLimitResponse:
"""In-memory all-or-nothing check-and-increment. Caller holds lock.
@ -1508,10 +1540,10 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
now_int = int(self._get_current_time().timestamp())
# Pass 1: read state, validate.
descriptor_state: list[dict[str, Any]] = []
descriptor_state: list[AtomicCounterState] = []
for meta in per_counter_meta:
window_size = meta["window_size"]
window_start = await self.internal_usage_cache.async_get_cache(
window_start: CachedCounterValue = await self.internal_usage_cache.async_get_cache(
key=meta["window_key"],
litellm_parent_otel_span=parent_otel_span,
local_only=True,
@ -1547,7 +1579,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
)
],
)
descriptor_state.append({"window_expired": window_expired, "current": current_counter})
descriptor_state.append(AtomicCounterState(window_expired=window_expired, current=current_counter))
# Pass 2: apply increments.
statuses: list[RateLimitStatus] = []
@ -1920,7 +1952,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
"""
return rpm_limit_type == "dynamic" or tpm_limit_type == "dynamic"
def _get_agent_from_registry(self, agent_id: str) -> Any | None:
def _get_agent_from_registry(self, agent_id: str) -> "AgentResponse | None":
"""Look up an agent from the in-memory registry by ID."""
from litellm.proxy.agent_endpoints.agent_registry import global_agent_registry
@ -1969,8 +2001,8 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
if agent is None:
return descriptors
agent_rpm = getattr(agent, "rpm_limit", None)
agent_tpm = getattr(agent, "tpm_limit", None)
agent_rpm = agent.rpm_limit
agent_tpm = agent.tpm_limit
if agent_rpm is not None or agent_tpm is not None:
descriptors.append(
RateLimitDescriptor(
@ -1984,8 +2016,8 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
)
)
session_rpm = getattr(agent, "session_rpm_limit", None)
session_tpm = getattr(agent, "session_tpm_limit", None)
session_rpm = agent.session_rpm_limit
session_tpm = agent.session_tpm_limit
if session_rpm is not None or session_tpm is not None:
session_id = self._get_session_id_from_data(data)
if session_id is not None:
@ -2244,7 +2276,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
# Fail safe: enforce limits if we can't check
return True
def get_rate_limiter_for_call_type(self, call_type: str) -> Any | None:
def get_rate_limiter_for_call_type(self, call_type: str) -> CustomLogger | None:
"""Get the rate limiter for the call type."""
if call_type == "acreate_batch":
batch_limiter = self._get_batch_rate_limiter()
@ -2372,8 +2404,8 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
user_api_key_dict: UserAPIKeyAuth,
cache: DualCache,
data: dict,
call_type: str,
):
call_type: CallTypesLiteral,
) -> Exception | str | dict[str, object] | None:
"""
Pre-call hook to check rate limits before making the API call.
Supports dynamic rate limiting based on deployment health.
@ -2698,8 +2730,8 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
# Get operations for this hash tag group
group_operations = [op for op in pipeline_operations if op["key"] in group_keys]
keys = []
args = []
keys: list[str] = []
args: list[float] = []
for op in group_operations:
# Convert None TTL to 0 for Lua script
@ -2767,15 +2799,15 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
@staticmethod
def _merge_ratelimit_statuses_into_additional_headers(
additional_headers: dict[str, Any],
additional_headers: dict[str, object],
statuses: list[RateLimitStatus],
) -> dict[str, Any]:
) -> dict[str, object]:
"""
Return ``additional_headers`` extended with
``x-ratelimit-{descriptor_key}-{remaining|limit}-{rate_limit_type}``
entries. Non-mutating so callers pick their own target dict.
"""
merged: dict[str, Any] = dict(additional_headers)
merged: dict[str, object] = dict(additional_headers)
for status in statuses:
prefix = f"x-ratelimit-{status['descriptor_key']}"
merged[f"{prefix}-remaining-{status['rate_limit_type']}"] = status["limit_remaining"]
@ -2785,7 +2817,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
def _collect_tpm_scope_targets(
self,
standard_logging_metadata: dict[str, Any],
kwargs: Any,
kwargs: object,
model_group: str | None,
) -> list[tuple[str, str]]:
"""
@ -2882,8 +2914,8 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
def _build_success_event_pipeline_operations(
self,
kwargs: Any,
response_obj: Any,
kwargs: dict[str, object],
response_obj: object,
rate_limit_type: Literal["output", "input", "total"],
) -> list[RedisPipelineIncrementOperation]:
"""Build Redis pipeline increment ops for TPM / parallel-request counters."""
@ -2894,7 +2926,9 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
# Get metadata from standard_logging_object - this correctly handles both
# 'metadata' and 'litellm_metadata' fields from litellm_params
standard_logging_object = kwargs.get("standard_logging_object") or {}
standard_logging_metadata = standard_logging_object.get("metadata") or {}
standard_logging_metadata = (
standard_logging_object.get("metadata") or {} if isinstance(standard_logging_object, dict) else {}
)
model_group = get_model_group_from_litellm_kwargs(kwargs)
@ -3008,9 +3042,9 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
async def async_logging_hook(
self,
kwargs: dict,
result: Any,
result: object,
call_type: str,
) -> tuple[dict, Any]:
) -> tuple[dict, object]:
"""
Mirror the pre-call rate-limit snapshot into the SLP so streaming
success callbacks see the same ``x-ratelimit-*`` headers the
@ -3027,8 +3061,8 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger):
def _mirror_ratelimit_response_into_logging_payload(
self,
kwargs: Any,
response_obj: Any,
kwargs: object,
response_obj: object,
) -> None:
"""
Copy the stashed ``RateLimitResponse`` into the SLP's

View file

@ -13,8 +13,8 @@ model/{model_id}/update - PATCH endpoint for model update.
import asyncio
import datetime
import json
from collections.abc import Mapping, Sequence
from typing import Any, Literal, cast
from collections.abc import Awaitable, Mapping, Sequence
from typing import Literal, Protocol, TypeVar, cast
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
from pydantic import BaseModel, ConfigDict, Field
@ -55,6 +55,7 @@ from litellm.proxy.management_endpoints.team_endpoints import (
)
from litellm.proxy.management_helpers.audit_logs import create_object_audit_log
from litellm.proxy.utils import PrismaClient, ProxyLogging
from litellm.repositories.base_repository import BaseRepository
from litellm.repositories.model_repository import ModelRepository
from litellm.repositories.table_repositories import ModelTableRepository
from litellm.repositories.team_repository import TeamRepository
@ -64,6 +65,7 @@ from litellm.router_utils.auto_router_model_naming import (
validate_strategy_router_model_write,
)
from litellm.types.proxy.management_endpoints.model_management_endpoints import (
PrismaCompatibleCreateDBModel,
UpdateUsefulLinksRequest,
)
from litellm.types.router import (
@ -78,6 +80,67 @@ from litellm.utils import get_utc_datetime
router = APIRouter()
_PrismaRowT = TypeVar("_PrismaRowT")
_RepositoryModelT = TypeVar("_RepositoryModelT", bound=BaseModel)
class _PrismaTableActions(Protocol[_PrismaRowT]):
async def find_unique(
self,
*,
where: Mapping[str, object],
include: Mapping[str, object] | None = None,
) -> _PrismaRowT | None: ...
async def find_many(
self,
*,
where: Mapping[str, object] | None = None,
include: Mapping[str, object] | None = None,
) -> list[_PrismaRowT]: ...
async def create(self, *, data: Mapping[str, object]) -> _PrismaRowT: ...
async def update(
self,
*,
where: Mapping[str, object],
data: Mapping[str, object],
include: Mapping[str, object] | None = None,
) -> _PrismaRowT: ...
async def delete(self, *, where: Mapping[str, object]) -> _PrismaRowT | None: ...
async def delete_many(self, *, where: Mapping[str, object]) -> int: ...
class _TeamModelAliasTeam(Protocol):
team_id: str
class _TeamModelAliasRow(Protocol):
id: int
model_aliases: dict[str, str]
team: _TeamModelAliasTeam | None
def _prisma_table(
repository: BaseRepository[_RepositoryModelT],
) -> _PrismaTableActions[_RepositoryModelT]:
return repository.table # any-ok: repository.table is the untyped Prisma client wrapper
def _model_alias_table(prisma_client: PrismaClient) -> _PrismaTableActions[_TeamModelAliasRow]:
return ModelTableRepository(prisma_client).table # any-ok: untyped Prisma client wrapper
def _transaction_model_table(transaction: object) -> _PrismaTableActions[LiteLLM_ProxyModelTable]:
return getattr(transaction, "litellm_proxymodeltable") # any-ok: untyped Prisma transaction client
def _audit_log_json(row: object) -> str | None:
return row.model_dump_json(exclude_none=True) if isinstance(row, BaseModel) else None
async def update_team(*args, **kwargs):
"""
@ -96,10 +159,7 @@ class UpdatePublicModelGroupsRequest(BaseModel):
async def get_db_model(model_id: str, prisma_client: PrismaClient) -> Deployment | None:
db_model = cast(
BaseModel | None,
await ModelRepository(prisma_client).table.find_unique(where={"model_id": model_id}),
)
db_model = await _prisma_table(ModelRepository(prisma_client)).find_unique(where={"model_id": model_id})
if not db_model:
return None
@ -322,7 +382,7 @@ async def patch_model(
update_data["updated_at"] = cast(str, get_utc_datetime())
# Perform partial update
updated_model = await ModelRepository(prisma_client).table.update(
updated_model = await _prisma_table(ModelRepository(prisma_client)).update(
where={"model_id": model_id},
data=update_data,
)
@ -425,7 +485,7 @@ async def _set_model_blocked_status(
param=None,
)
updated_model = await ModelRepository(prisma_client).table.update(
updated_model = await _prisma_table(ModelRepository(prisma_client)).update(
where={"model_id": data.model_id},
data={
"blocked": blocked,
@ -444,9 +504,7 @@ async def _set_model_blocked_status(
user_api_key_dict=user_api_key_dict,
table_name=LitellmTableNames.PROXY_MODEL_TABLE_NAME,
before_value=db_model.model_dump_json(exclude_none=True),
after_value=(
updated_model.model_dump_json(exclude_none=True) if isinstance(updated_model, BaseModel) else None
),
after_value=_audit_log_json(updated_model),
litellm_changed_by=litellm_changed_by,
litellm_proxy_admin_name=litellm_proxy_admin_name,
)
@ -552,24 +610,20 @@ async def _add_model_to_db(
for k, v in _litellm_params_dict.items():
encrypted_value = encrypt_value_helper(value=v, new_encryption_key=new_encryption_key)
model_params.litellm_params[k] = encrypted_value
_data: dict = {
_data: PrismaCompatibleCreateDBModel = {
"model_id": model_params.model_info.id,
"model_name": model_params.model_name,
"litellm_params": model_params.litellm_params.model_dump_json(exclude_none=True), # type: ignore
"model_info": model_params.model_info.model_dump_json( # type: ignore
exclude_none=True
),
"litellm_params": model_params.litellm_params.model_dump_json(exclude_none=True),
"model_info": model_params.model_info.model_dump_json(exclude_none=True),
"created_by": user_api_key_dict.user_id or LITELLM_PROXY_ADMIN_NAME,
"updated_by": user_api_key_dict.user_id or LITELLM_PROXY_ADMIN_NAME,
}
if model_params.model_info.id is not None:
_data["model_id"] = model_params.model_info.id
if should_create_model_in_db:
model_response = await ModelRepository(prisma_client).table.create(
data=_data # type: ignore
)
model_response = await _prisma_table(ModelRepository(prisma_client)).create(data=_data)
else:
model_response = LiteLLM_ProxyModelTable(**_data)
model_response = LiteLLM_ProxyModelTable.model_validate(dict(_data))
return model_response
@ -757,7 +811,9 @@ async def _setup_new_team_model_assignment(
async def _get_team_deployments(
team_id: str, prisma_client: PrismaClient, table: Any | None = None
team_id: str,
prisma_client: PrismaClient,
table: _PrismaTableActions[LiteLLM_ProxyModelTable] | None = None,
) -> list[LiteLLM_ProxyModelTable]:
"""
Fetch all deployments for a given team_id from the database.
@ -773,8 +829,8 @@ async def _get_team_deployments(
existing transaction.
"""
prefix = f"model_name_{team_id}_"
table = table or ModelRepository(prisma_client).table
response = await table.find_many(
model_table = table or _prisma_table(ModelRepository(prisma_client))
response = await model_table.find_many(
where={
"model_name": {"startswith": prefix},
}
@ -783,7 +839,7 @@ async def _get_team_deployments(
return []
# Confirm team_id in model_info (defensive check)
result = []
result: list[LiteLLM_ProxyModelTable] = []
for row in response:
model_info = model_info_as_mapping(row.model_info)
if model_info is not None and model_info.get("team_id") == team_id:
@ -794,7 +850,7 @@ async def _get_team_deployments(
async def delete_team_models(
team_ids: list[str],
prisma_client: PrismaClient,
llm_router: Any | None,
llm_router: Router | None,
) -> list[str]:
"""
Delete every BYOK model owned by the given teams, from the DB and the router.
@ -809,11 +865,12 @@ async def delete_team_models(
"""
deleted_model_ids: list[str] = []
async with prisma_client.db.tx() as tx:
tx_model_table = _transaction_model_table(tx)
for team_id in team_ids:
rows = await _get_team_deployments(team_id, prisma_client, table=tx.litellm_proxymodeltable)
rows = await _get_team_deployments(team_id, prisma_client, table=tx_model_table)
model_ids = [row.model_id for row in rows]
if model_ids:
await tx.litellm_proxymodeltable.delete_many(where={"model_id": {"in": model_ids}})
await tx_model_table.delete_many(where={"model_id": {"in": model_ids}})
deleted_model_ids.extend(model_ids)
if deleted_model_ids:
@ -908,14 +965,15 @@ async def _remove_unbacked_team_models(
if not names_to_remove:
return
existing_team_row = await prisma_client.db.litellm_teamtable.find_unique(where={"team_id": team_id})
team_table = _prisma_table(TeamRepository(prisma_client))
existing_team_row = await team_table.find_unique(where={"team_id": team_id})
if existing_team_row is None:
return
updated_team_row: LiteLLM_TeamTable = await prisma_client.db.litellm_teamtable.update(
updated_team_row: LiteLLM_TeamTable = await team_table.update(
where={"team_id": team_id},
data={"models": [model for model in existing_team_row.models if model not in names_to_remove]},
include={"object_permission": True}, # type: ignore
include={"object_permission": True},
)
await _refresh_cached_team(
team_row=updated_team_row,
@ -1050,7 +1108,7 @@ class ModelManagementAuthChecks:
detail={"error": CommonProxyErrors.not_premium_user.value},
)
_existing_team_row = await TeamRepository(prisma_client).table.find_unique(
_existing_team_row = await _prisma_table(TeamRepository(prisma_client)).find_unique(
where={"team_id": model_params.model_info.team_id}
)
@ -1079,7 +1137,7 @@ class ModelManagementAuthChecks:
) -> Literal[True]:
## Check team model auth
if model_params.model_info is not None and model_params.model_info.team_id is not None:
team_obj_row = await TeamRepository(prisma_client).table.find_unique(
team_obj_row = await _prisma_table(TeamRepository(prisma_client)).find_unique(
where={"team_id": model_params.model_info.team_id}
)
if team_obj_row is None:
@ -1157,7 +1215,7 @@ async def delete_model(
},
)
model_in_db = await ModelRepository(prisma_client).table.find_unique(where={"model_id": model_info.id})
model_in_db = await _prisma_table(ModelRepository(prisma_client)).find_unique(where={"model_id": model_info.id})
if model_in_db is None:
raise HTTPException(
status_code=400,
@ -1180,7 +1238,7 @@ async def delete_model(
- store keys separately
"""
# encrypt litellm params #
result = await ModelRepository(prisma_client).table.delete(where={"model_id": model_info.id})
result = await _prisma_table(ModelRepository(prisma_client)).delete(where={"model_id": model_info.id})
if result is None:
raise HTTPException(
@ -1253,9 +1311,9 @@ async def delete_team_model_alias(
Returns:
- List of team id + model alias pairs that were removed
"""
team_model_aliases = await ModelTableRepository(prisma_client).table.find_many(include={"team": True})
tasks = []
removed_model_aliases = []
team_model_aliases = await _model_alias_table(prisma_client).find_many(include={"team": True})
tasks: list[Awaitable[_TeamModelAliasRow]] = []
removed_model_aliases: list[tuple[str, str]] = []
for team_model_alias in team_model_aliases:
model_aliases = team_model_alias.model_aliases # {"alias": "public model name"}
id = team_model_alias.id
@ -1266,7 +1324,7 @@ async def delete_team_model_alias(
removed_model_aliases.append((team_model_alias.team.team_id, key))
del model_aliases[key]
tasks.append(
ModelTableRepository(prisma_client).table.update(
_model_alias_table(prisma_client).update(
where={"id": id},
data={"model_aliases": json.dumps(model_aliases)},
)
@ -1411,9 +1469,7 @@ async def add_new_model(
user_api_key_dict=user_api_key_dict,
table_name=LitellmTableNames.PROXY_MODEL_TABLE_NAME,
before_value=None,
after_value=(
model_response.model_dump_json(exclude_none=True) if isinstance(model_response, BaseModel) else None
),
after_value=_audit_log_json(model_response),
litellm_changed_by=user_api_key_dict.user_id,
litellm_proxy_admin_name=LITELLM_PROXY_ADMIN_NAME,
)
@ -1481,7 +1537,7 @@ async def update_model(
)
_model_id = None
_model_info = getattr(model_params, "model_info", None)
_model_info = model_params.model_info
if _model_info is None:
raise Exception("model_info not provided")
@ -1489,7 +1545,9 @@ async def update_model(
if _model_id is None:
raise Exception("model_info.id not provided")
_existing_litellm_params = await ModelRepository(prisma_client).table.find_unique(where={"model_id": _model_id})
_existing_litellm_params = await _prisma_table(ModelRepository(prisma_client)).find_unique(
where={"model_id": _model_id}
)
if _existing_litellm_params is None:
if llm_router is not None and llm_router.get_deployment(model_id=_model_id) is not None:
@ -1543,7 +1601,7 @@ async def update_model(
"litellm_params": json.dumps(merged_dictionary), # type: ignore
"updated_by": user_api_key_dict.user_id or LITELLM_PROXY_ADMIN_NAME,
}
model_response = await ModelRepository(prisma_client).table.update(
model_response = await _prisma_table(ModelRepository(prisma_client)).update(
where={"model_id": _model_id},
data=_data, # type: ignore
)
@ -1558,16 +1616,8 @@ async def update_model(
action="updated",
user_api_key_dict=user_api_key_dict,
table_name=LitellmTableNames.PROXY_MODEL_TABLE_NAME,
before_value=(
_existing_litellm_params.model_dump_json(exclude_none=True)
if isinstance(_existing_litellm_params, BaseModel)
else None
),
after_value=(
model_response.model_dump_json(exclude_none=True)
if isinstance(model_response, BaseModel)
else None
),
before_value=_audit_log_json(_existing_litellm_params),
after_value=_audit_log_json(model_response),
litellm_changed_by=user_api_key_dict.user_id,
litellm_proxy_admin_name=LITELLM_PROXY_ADMIN_NAME,
)
@ -1787,7 +1837,7 @@ def model_info_as_mapping(model_info: object) -> Mapping[str, object] | None:
if not isinstance(model_info, str):
return None
try:
parsed = json.loads(model_info)
parsed: object = json.loads(model_info)
except (TypeError, ValueError):
return None
return parsed if isinstance(parsed, Mapping) else None

View file

@ -32,7 +32,9 @@ from __future__ import annotations
import json
import re
from typing import Any
from collections.abc import Sequence
from operator import attrgetter
from typing import TYPE_CHECKING, Protocol, TypeVar
from urllib.parse import quote, unquote
from fastapi import HTTPException
@ -46,11 +48,24 @@ from litellm.proxy._types import UserAPIKeyAuth
from litellm.repositories.table_repositories import (
ManagedFileRepository,
ManagedObjectRepository,
PrismaTableRepository,
)
from litellm.types.llms.openai import OpenAIFileObject
from litellm.types.passthrough_endpoints.managed_ids import (
ManagedResourceOwner,
ManagedResourceRow,
ManagedResourceTable,
PassthroughListResponse,
PrismaWhere,
)
from .managed_id_codec import ManagedIdPayload, decode, is_managed, new_managed_id
if TYPE_CHECKING:
from litellm.proxy.utils import PrismaClient
_KeyT = TypeVar("_KeyT")
# ---------------------------------------------------------------------------
# Field map
# ---------------------------------------------------------------------------
@ -172,7 +187,7 @@ class _RawIdGuardBudget:
def __init__(self, limit: int = _MAX_RAW_ID_GUARD_LOOKUPS) -> None:
self._remaining = limit
self._seen: set = set()
self._seen: set[str] = set()
def reserve(self, raw_id: str) -> bool:
"""Return True when a guard lookup for *raw_id* should run. Returns
@ -254,6 +269,76 @@ def _canonical_path(route: str) -> str:
return stripped
def _as_str_keyed_dict(value: object) -> dict[str, object] | None:
"""Narrow a decoded-JSON value to a mapping, or ``None`` when it is not one."""
if isinstance(value, dict):
return value
return None
class _ManagedFileLookup(Protocol):
"""The enterprise managed-files hook's cached file lookup."""
async def __call__(self, file_id: str, litellm_parent_otel_span: None = None) -> ManagedResourceOwner | None: ...
class _ManagedFileStore(Protocol):
"""The enterprise managed-files hook's file writer."""
async def __call__(
self,
*,
file_id: str,
file_object: OpenAIFileObject | None,
litellm_parent_otel_span: None,
model_mappings: dict[str, str],
user_api_key_dict: UserAPIKeyAuth,
) -> None: ...
async def _hook_get_unified_file_id(managed_files_hook: object, managed_id: str) -> ManagedResourceOwner | None:
"""Look a managed file up in the enterprise hook's cache.
The proxy hook registry hands the hook back as a bare ``CustomLogger``, so
the managed-files API is resolved by name; a hook that does not implement it
raises ``AttributeError`` exactly as a direct attribute access would.
"""
lookup: _ManagedFileLookup = attrgetter("get_unified_file_id")(managed_files_hook) # any-ok: untyped hook registry
return await lookup(managed_id, litellm_parent_otel_span=None)
async def _hook_store_unified_file_id(
managed_files_hook: object,
file_id: str,
file_object: OpenAIFileObject | None,
model_mappings: dict[str, str],
user_api_key_dict: UserAPIKeyAuth,
) -> None:
"""Persist a managed file row through the enterprise hook, resolved by name
for the same reason as ``_hook_get_unified_file_id``."""
store: _ManagedFileStore = attrgetter("store_unified_file_id")(managed_files_hook) # any-ok: untyped hook registry
await store(
file_id=file_id,
file_object=file_object,
litellm_parent_otel_span=None,
model_mappings=model_mappings,
user_api_key_dict=user_api_key_dict,
)
def _managed_table(repository: PrismaTableRepository) -> ManagedResourceTable:
"""Typed view of a repository's Prisma table actions."""
return repository.table # any-ok: Prisma actions are reached through the untyped client wrapper
def _managed_file_table(prisma_client: PrismaClient) -> ManagedResourceTable:
return _managed_table(ManagedFileRepository(prisma_client))
def _managed_object_table(prisma_client: PrismaClient) -> ManagedResourceTable:
return _managed_table(ManagedObjectRepository(prisma_client))
# ---------------------------------------------------------------------------
# Shared resolver — used by all INPUT path extractors
# ---------------------------------------------------------------------------
@ -263,8 +348,8 @@ async def _resolve_one(
managed_id: str,
provider: str,
user_api_key_dict: UserAPIKeyAuth,
prisma_client: Any,
managed_files_hook: Any,
prisma_client: PrismaClient | None,
managed_files_hook: object,
) -> str:
"""
Resolve a single value that may be a passthrough managed ID.
@ -307,10 +392,7 @@ async def _resolve_one(
# File table — use hook's internal cache for speed when available
if managed_files_hook is not None:
try:
file_row = await managed_files_hook.get_unified_file_id(
managed_id,
litellm_parent_otel_span=None,
)
file_row = await _hook_get_unified_file_id(managed_files_hook, managed_id)
if file_row is not None:
row_created_by = file_row.created_by
row_team_id = file_row.team_id
@ -322,9 +404,7 @@ async def _resolve_one(
)
if not found and prisma_client is not None:
try:
db_row = await ManagedFileRepository(prisma_client).table.find_first(
where={"unified_file_id": managed_id}
)
db_row = await _managed_file_table(prisma_client).find_first(where={"unified_file_id": managed_id})
if db_row is not None:
row_created_by = db_row.created_by
row_team_id = db_row.team_id
@ -338,9 +418,7 @@ async def _resolve_one(
# Object table (batches, responses)
if prisma_client is not None:
try:
obj_row = await ManagedObjectRepository(prisma_client).table.find_first(
where={"unified_object_id": managed_id}
)
obj_row = await _managed_object_table(prisma_client).find_first(where={"unified_object_id": managed_id})
if obj_row is not None:
row_created_by = obj_row.created_by
row_team_id = obj_row.team_id
@ -372,7 +450,7 @@ async def _guard_raw_provider_id(
raw_id: str,
provider: str,
user_api_key_dict: UserAPIKeyAuth,
prisma_client: Any,
prisma_client: PrismaClient | None,
budget: _RawIdGuardBudget | None = None,
) -> None:
"""Deny a raw provider ID that maps to a managed resource the caller does
@ -398,7 +476,7 @@ async def _guard_raw_provider_id(
# id and scope to the current provider in the application layer (same as
# _mint_or_reuse_file's dedup).
try:
candidates = await ManagedFileRepository(prisma_client).table.find_many(
candidates = await _managed_file_table(prisma_client).find_many(
where={"flat_model_file_ids": {"has": raw_id}},
)
except Exception:
@ -419,7 +497,7 @@ async def _guard_raw_provider_id(
# Object rows store model_object_id as "passthrough:{provider}:{raw}", so
# the lookup is exact and already provider-scoped.
try:
existing = await ManagedObjectRepository(prisma_client).table.find_first(
existing = await _managed_object_table(prisma_client).find_first(
where={"model_object_id": f"passthrough:{provider}:{raw_id}"}
)
except Exception:
@ -434,7 +512,7 @@ async def _guard_raw_provider_id(
# ---------------------------------------------------------------------------
def _build_managed_file_object(snapshot: dict[str, Any] | None, managed_id: str) -> OpenAIFileObject | None:
def _build_managed_file_object(snapshot: dict[str, object] | None, managed_id: str) -> OpenAIFileObject | None:
"""Build an ``OpenAIFileObject`` (with the managed ID swapped in) from an
upstream file response so the DB-served list returns the same metadata as a
direct file GET. Returns ``None`` when no usable snapshot is available, in
@ -442,7 +520,7 @@ def _build_managed_file_object(snapshot: dict[str, Any] | None, managed_id: str)
if not snapshot:
return None
try:
return OpenAIFileObject(**{**snapshot, "id": managed_id})
return OpenAIFileObject.model_validate({**snapshot, "id": managed_id})
except Exception:
verbose_proxy_logger.debug(
"managed_id_rewriter: file object snapshot incomplete; storing file row without list metadata",
@ -455,9 +533,9 @@ async def _mint_or_reuse_file(
raw_id: str,
provider: str,
user_api_key_dict: UserAPIKeyAuth,
prisma_client: Any,
managed_files_hook: Any,
file_object_snapshot: dict[str, Any] | None = None,
prisma_client: PrismaClient | None,
managed_files_hook: object,
file_object_snapshot: dict[str, object] | None = None,
is_create_route: bool = True,
) -> str:
"""Return an existing managed file ID or mint + store a new one."""
@ -479,7 +557,7 @@ async def _mint_or_reuse_file(
# reuse a stable row instead of minting duplicate rows on every call.
if prisma_client is not None:
try:
candidates = await ManagedFileRepository(prisma_client).table.find_many(
candidates = await _managed_file_table(prisma_client).find_many(
where={"flat_model_file_ids": {"has": raw_id}},
order={"created_at": "asc"},
)
@ -525,10 +603,10 @@ async def _mint_or_reuse_file(
)
if managed_files_hook is not None:
try:
await managed_files_hook.store_unified_file_id(
await _hook_store_unified_file_id(
managed_files_hook,
file_id=managed_id,
file_object=_build_managed_file_object(file_object_snapshot, managed_id),
litellm_parent_otel_span=None,
model_mappings={
_passthrough_sentinel_model_id(provider): raw_id,
_PASSTHROUGH_PROVIDER_MARKER_KEY: _passthrough_provider_marker(provider),
@ -551,9 +629,9 @@ async def _mint_or_reuse_object(
raw_id: str,
provider: str,
file_purpose: str,
body_snapshot: dict,
body_snapshot: dict[str, object],
user_api_key_dict: UserAPIKeyAuth,
prisma_client: Any,
prisma_client: PrismaClient | None,
is_create_route: bool,
) -> str:
"""Return an existing managed object ID (batch/response) or mint + store one."""
@ -569,7 +647,7 @@ async def _mint_or_reuse_object(
# f"{purpose}:{provider}:{raw_id}" for the same reason.
namespaced_model_object_id = f"passthrough:{provider}:{raw_id}"
async def _reuse_existing(existing: Any, refresh_snapshot: bool) -> str:
async def _reuse_existing(existing: ManagedResourceRow, refresh_snapshot: bool) -> str:
"""Resolve an already-persisted namespaced row: enforce the access
check, optionally refresh the snapshot, and return its managed ID."""
if not can_access_resource(user_api_key_dict, existing.created_by, existing.team_id):
@ -598,7 +676,7 @@ async def _mint_or_reuse_object(
# the batch's latest state (e.g. output_file_id / error_file_id that
# were null at creation but populated once the batch completed).
try:
await ManagedObjectRepository(prisma_client).table.update(
await _managed_object_table(prisma_client).update(
where={"unified_object_id": existing.unified_object_id},
data={
"file_object": json.dumps(body_snapshot),
@ -618,7 +696,7 @@ async def _mint_or_reuse_object(
# Dedup: look up by the namespaced key — guaranteed unique per provider.
try:
existing = await ManagedObjectRepository(prisma_client).table.find_first(
existing = await _managed_object_table(prisma_client).find_first(
where={"model_object_id": namespaced_model_object_id}
)
except Exception:
@ -635,7 +713,7 @@ async def _mint_or_reuse_object(
raw_id.split("_", 1)[0],
)
try:
await ManagedObjectRepository(prisma_client).table.upsert(
await _managed_object_table(prisma_client).upsert(
where={"unified_object_id": managed_id},
data={
"create": {
@ -659,7 +737,7 @@ async def _mint_or_reuse_object(
# the winner's managed ID so both callers converge on one ID instead of
# the loser silently keeping the raw id.
try:
raced = await ManagedObjectRepository(prisma_client).table.find_first(
raced = await _managed_object_table(prisma_client).find_first(
where={"model_object_id": namespaced_model_object_id}
)
except Exception:
@ -681,11 +759,11 @@ async def rewrite_response_ids(
provider: str,
method: str,
route: str,
body: dict,
body: dict[str, object],
user_api_key_dict: UserAPIKeyAuth,
prisma_client: Any,
managed_files_hook: Any,
) -> dict:
prisma_client: PrismaClient | None,
managed_files_hook: object,
) -> dict[str, object]:
"""
Mint managed IDs for raw provider values listed in
``BUILTIN_OUTPUT_ID_FIELD_MAP`` and swap them into *body*.
@ -795,7 +873,7 @@ def is_passthrough_list_route(provider: str, method: str, route: str) -> bool:
return (provider, canonical) in _LIST_ROUTE_TABLE
def _parse_file_object(file_object: Any) -> Any:
def _parse_file_object(file_object: object) -> object:
"""Prisma may return ``Json`` columns as either a parsed dict or the raw
JSON string (depending on driver / row source). Mirror the handling used
elsewhere (see ``openai_files_endpoints/common_utils.py``) so callers can
@ -803,13 +881,13 @@ def _parse_file_object(file_object: Any) -> Any:
"""
if isinstance(file_object, str):
try:
return json.loads(file_object)
return json.loads(file_object) # any-ok: json.loads -> Any
except (TypeError, ValueError):
return None
return file_object
def _empty_list_response() -> dict[str, Any]:
def _empty_list_response() -> PassthroughListResponse:
return {
"object": "list",
"data": [],
@ -819,7 +897,7 @@ def _empty_list_response() -> dict[str, Any]:
}
def _parse_list_limit(query_params: dict[str, Any] | None) -> tuple[int, int]:
def _parse_list_limit(query_params: dict[str, str] | None) -> tuple[int, int]:
params = query_params or {}
try:
raw_limit = int(params.get("limit", 20))
@ -830,17 +908,17 @@ def _parse_list_limit(query_params: dict[str, Any] | None) -> tuple[int, int]:
async def _build_list_where_with_cursor(
prisma_client: Any,
prisma_client: PrismaClient,
resource_kind: str,
provider: str,
owner_filter: dict[str, Any],
query_params: dict[str, Any] | None,
) -> tuple[dict[str, Any], str]:
owner_filter: PrismaWhere,
query_params: dict[str, str] | None,
) -> tuple[PrismaWhere, str]:
"""Return a Prisma ``where`` clause and fetch order for a list query."""
params = query_params or {}
after_id: str | None = params.get("after")
before_id: str | None = params.get("before")
where: dict[str, Any] = dict(owner_filter)
where: PrismaWhere = dict(owner_filter)
fetch_order = "desc"
cursor_id = after_id or before_id
@ -851,9 +929,7 @@ async def _build_list_where_with_cursor(
return where, fetch_order
cursor_table = (
ManagedFileRepository(prisma_client).table
if resource_kind == "files"
else ManagedObjectRepository(prisma_client).table
_managed_file_table(prisma_client) if resource_kind == "files" else _managed_object_table(prisma_client)
)
cursor_field = "unified_file_id" if resource_kind == "files" else "unified_object_id"
try:
@ -867,7 +943,7 @@ async def _build_list_where_with_cursor(
# created_at is not unique, so the boundary must also compare the
# unique id (the secondary sort key) to avoid skipping or repeating
# rows that share the cursor row's timestamp across a page boundary.
boundary = {
boundary: PrismaWhere = {
"OR": [
{"created_at": {op: cursor_row.created_at}},
{
@ -885,23 +961,23 @@ async def _build_list_where_with_cursor(
async def _fetch_list_rows(
prisma_client: Any,
prisma_client: PrismaClient,
resource_kind: str,
where: dict[str, Any],
where: PrismaWhere,
fetch_order: str,
fetch_limit: int,
) -> list[Any] | None:
) -> Sequence[ManagedResourceRow] | None:
# created_at is not unique, so a second sort on the unique id column gives a
# total order, keeping the limit+1 page boundary and cursor deterministic
# across rows that share a created_at timestamp.
try:
if resource_kind == "files":
return await ManagedFileRepository(prisma_client).table.find_many(
return await _managed_file_table(prisma_client).find_many(
where=where,
order=[{"created_at": fetch_order}, {"unified_file_id": fetch_order}],
take=fetch_limit,
)
return await ManagedObjectRepository(prisma_client).table.find_many(
return await _managed_object_table(prisma_client).find_many(
where={**where, "file_purpose": "batch"},
order=[{"created_at": fetch_order}, {"unified_object_id": fetch_order}],
take=fetch_limit,
@ -912,14 +988,14 @@ async def _fetch_list_rows(
async def _fetch_provider_scoped_list_rows(
prisma_client: Any,
prisma_client: PrismaClient,
resource_kind: str,
provider: str,
where: dict[str, Any],
where: PrismaWhere,
fetch_order: str,
raw_limit: int,
fetch_limit: int,
) -> tuple[list[Any], bool]:
) -> tuple[Sequence[ManagedResourceRow], bool]:
"""Fetch one page of list rows scoped to *provider* at the DB level.
Both resource kinds carry a provider-distinguishing value that the query
@ -933,7 +1009,7 @@ async def _fetch_provider_scoped_list_rows(
A DB failure returns an empty page (fail closed) so the caller never falls
through to the upstream provider.
"""
scoped_where = dict(where)
scoped_where: PrismaWhere = dict(where)
if resource_kind == "files":
scoped_where["flat_model_file_ids"] = {"has": _passthrough_provider_marker(provider)}
else:
@ -951,43 +1027,44 @@ async def _fetch_provider_scoped_list_rows(
return page, has_more
def _serialize_file_list_item(row: Any) -> dict[str, Any]:
item: dict[str, Any] = {
def _serialize_file_list_item(row: ManagedResourceRow) -> dict[str, object]:
item: dict[str, object] = {
"id": row.unified_file_id,
"object": "file",
"created_at": int(row.created_at.timestamp()) if row.created_at else None,
}
file_object = _parse_file_object(row.file_object)
if isinstance(file_object, dict):
file_object = _as_str_keyed_dict(_parse_file_object(row.file_object))
if file_object is not None:
item.update(file_object)
item["id"] = row.unified_file_id # managed ID always wins over stored raw id
return item
def _serialize_batch_list_item(row: Any) -> dict[str, Any]:
item: dict[str, Any] = {}
file_object = _parse_file_object(row.file_object)
if isinstance(file_object, dict):
def _serialize_batch_list_item(row: ManagedResourceRow) -> dict[str, object]:
item: dict[str, object] = {}
file_object = _as_str_keyed_dict(_parse_file_object(row.file_object))
if file_object is not None:
item.update(file_object)
item["id"] = row.unified_object_id # managed ID always wins
item["object"] = "batch"
return item
def _list_boundary_ids(rows: list[Any], resource_kind: str) -> tuple[str | None, str | None]:
def _list_boundary_ids(rows: Sequence[ManagedResourceRow], resource_kind: str) -> tuple[str | None, str | None]:
if not rows:
return None, None
id_attr = "unified_file_id" if resource_kind == "files" else "unified_object_id"
return getattr(rows[0], id_attr), getattr(rows[-1], id_attr)
if resource_kind == "files":
return rows[0].unified_file_id, rows[-1].unified_file_id
return rows[0].unified_object_id, rows[-1].unified_object_id
async def list_passthrough_ids_from_db(
provider: str,
route: str,
user_api_key_dict: UserAPIKeyAuth,
prisma_client: Any,
query_params: dict[str, Any] | None = None,
) -> dict[str, Any] | None:
prisma_client: PrismaClient | None,
query_params: dict[str, str] | None = None,
) -> PassthroughListResponse | None:
"""Query the DB for managed IDs the caller owns and return an OpenAI-style
paginated list response.
@ -1060,8 +1137,8 @@ async def rewrite_path_ids(
path: str,
provider: str,
user_api_key_dict: UserAPIKeyAuth,
prisma_client: Any,
managed_files_hook: Any,
prisma_client: PrismaClient | None,
managed_files_hook: object,
) -> str:
"""
Walk URL path segments and resolve any passthrough managed IDs to raw
@ -1092,12 +1169,12 @@ async def rewrite_path_ids(
async def rewrite_query_ids(
params: dict[str, Any] | None,
params: dict[str, object] | None,
provider: str,
user_api_key_dict: UserAPIKeyAuth,
prisma_client: Any,
managed_files_hook: Any,
) -> dict[str, Any] | None:
prisma_client: PrismaClient | None,
managed_files_hook: object,
) -> dict[str, object] | None:
"""
Walk query param values and resolve any passthrough managed IDs.
Returns *params* unchanged (same object) when nothing is resolved.
@ -1124,12 +1201,12 @@ async def rewrite_query_ids(
async def rewrite_body_ids(
body: dict[str, Any] | None,
body: dict[str, object] | None,
provider: str,
user_api_key_dict: UserAPIKeyAuth,
prisma_client: Any,
managed_files_hook: Any,
) -> dict[str, Any] | None:
prisma_client: PrismaClient | None,
managed_files_hook: object,
) -> dict[str, object] | None:
"""
Recursively walk a request body dict/list and resolve any passthrough
managed IDs. Skips litellm internal keys (``litellm_*``).
@ -1140,27 +1217,34 @@ async def rewrite_body_ids(
budget = _RawIdGuardBudget()
async def _walk(node: Any, depth: int) -> Any:
async def _walk_dict(node: dict[_KeyT, object], depth: int) -> dict[_KeyT, object]:
result: dict[_KeyT, object] = {}
changed_inner = False
for k, v in node.items():
# Skip litellm internal injection keys (e.g. litellm_logging_obj)
if isinstance(k, str) and k.startswith("litellm_"):
result[k] = v
continue
new_v = await _walk(v, depth + 1)
result[k] = new_v
if new_v is not v:
changed_inner = True
return result if changed_inner else node
async def _walk_list(node: list[object], depth: int) -> list[object]:
new_list = [await _walk(item, depth + 1) for item in node]
if any(n is not o for n, o in zip(new_list, node)):
return new_list
return node
async def _walk(node: object, depth: int) -> object:
if depth >= _MAX_BODY_REWRITE_DEPTH:
return node
if isinstance(node, dict):
result: dict[str, Any] = {}
changed_inner = False
for k, v in node.items():
# Skip litellm internal injection keys (e.g. litellm_logging_obj)
if isinstance(k, str) and k.startswith("litellm_"):
result[k] = v
continue
new_v = await _walk(v, depth + 1)
result[k] = new_v
if new_v is not v:
changed_inner = True
return result if changed_inner else node
node_dict = _as_str_keyed_dict(node)
if node_dict is not None:
return await _walk_dict(node_dict, depth)
elif isinstance(node, list):
new_list = [await _walk(item, depth + 1) for item in node]
if any(n is not o for n, o in zip(new_list, node)):
return new_list
return node
return await _walk_list(node, depth)
elif isinstance(node, str):
if is_managed(node):
return await _resolve_one(node, provider, user_api_key_dict, prisma_client, managed_files_hook)
@ -1168,7 +1252,7 @@ async def rewrite_body_ids(
return node
return node
rewritten = await _walk(body, 0)
rewritten = await _walk_dict(body, 0)
if rewritten is not body:
verbose_proxy_logger.debug("managed_id_rewriter: body ids rewritten provider=%s", provider)
return rewritten

View file

@ -4,7 +4,7 @@ Handles transforming from Responses API -> LiteLLM completion (Chat Completion
import json
import re
from collections.abc import Sequence
from collections.abc import Mapping, Sequence
from typing import Any, Literal, cast
from openai.types.responses import ResponseFunctionToolCall
@ -33,6 +33,8 @@ from litellm.types.llms.openai import (
ChatCompletionToolParam,
ChatCompletionUserMessage,
GenericChatCompletionMessage,
ImageURLListItem,
ImageURLObject,
InputTokensDetails,
OpenAIMcpServerTool,
OpenAIWebSearchOptions,
@ -47,6 +49,7 @@ from litellm.types.llms.openai import (
ValidChatCompletionMessageContentTypesLiteral,
)
from litellm.types.responses.main import (
ApplyPatchToolCallLike,
CustomToolCallOutputItem,
GenericResponseOutputItem,
GenericResponseOutputItemContentAnnotation,
@ -189,7 +192,7 @@ class LiteLLMCompletionResponsesConfig:
responses_api_request: ResponsesAPIOptionalRequestParams,
custom_llm_provider: str | None = None,
stream: bool | None = None,
extra_headers: dict[str, Any] | None = None,
extra_headers: dict[str, object] | None = None,
**kwargs,
) -> dict:
"""
@ -446,7 +449,9 @@ class LiteLLMCompletionResponsesConfig:
if not chat_completion_messages:
continue
deduped_in_place: list[Any] = []
deduped_in_place: list[
AllMessageValues | GenericChatCompletionMessage | ChatCompletionResponseMessage
] = []
for m in chat_completion_messages:
role = ""
if isinstance(m, dict):
@ -456,7 +461,7 @@ class LiteLLMCompletionResponsesConfig:
# Drop assistant tool_calls wrappers if we already have this call_id
if role == "assistant":
tool_calls: Any = (
tool_calls: object = (
m.get("tool_calls") if isinstance(m, dict) else getattr(m, "tool_calls", None)
)
call_id = ""
@ -518,7 +523,7 @@ class LiteLLMCompletionResponsesConfig:
call_id = ""
if role == "assistant":
tool_calls: Any = None
tool_calls: object = None
if isinstance(tool_call_message, dict):
tool_calls = tool_call_message.get("tool_calls")
else:
@ -562,7 +567,16 @@ class LiteLLMCompletionResponsesConfig:
return False
@staticmethod
def _find_previous_assistant_idx(messages: list[Any], current_idx: int) -> int | None:
def _find_previous_assistant_idx(
messages: Sequence[
AllMessageValues
| GenericChatCompletionMessage
| ChatCompletionResponseMessage
| ChatCompletionMessageToolCall
| Message
],
current_idx: int,
) -> int | None:
"""Find the index of the previous assistant message."""
for j in range(current_idx - 1, -1, -1):
if messages[j].get("role") == "assistant":
@ -570,17 +584,19 @@ class LiteLLMCompletionResponsesConfig:
return None
@staticmethod
def _recover_tool_call_id_from_assistant(assistant_message: Any, message: Any) -> str:
def _recover_tool_call_id_from_assistant(assistant_message: object, message: object) -> str:
"""Try to recover empty tool_call_id from assistant message's tool_calls."""
tool_calls_raw = (
assistant_message.get("tool_calls")
if isinstance(assistant_message, dict)
else getattr(assistant_message, "tool_calls", None)
)
if tool_calls_raw and isinstance(tool_calls_raw, list) and len(tool_calls_raw) > 0:
first_tool_call = tool_calls_raw[0]
if isinstance(first_tool_call, dict):
tool_call_id_raw = first_tool_call.get("id", "")
tool_calls_items = LiteLLMCompletionResponsesConfig._as_object_list(tool_calls_raw)
if tool_calls_raw and tool_calls_items is not None and len(tool_calls_items) > 0:
first_tool_call = tool_calls_items[0]
first_tool_call_mapping = LiteLLMCompletionResponsesConfig._as_object_mapping(first_tool_call)
if first_tool_call_mapping is not None:
tool_call_id_raw: object = first_tool_call_mapping.get("id", "")
return str(tool_call_id_raw) if tool_call_id_raw is not None else ""
elif hasattr(first_tool_call, "id"):
tool_call_id_raw = getattr(first_tool_call, "id", None)
@ -588,7 +604,7 @@ class LiteLLMCompletionResponsesConfig:
return ""
@staticmethod
def _get_tool_calls_list(assistant_message: Any) -> list[Any]:
def _get_tool_calls_list(assistant_message: object) -> list[object]:
"""Extract tool_calls as a list from assistant message."""
tool_calls_raw = (
assistant_message.get("tool_calls")
@ -597,14 +613,15 @@ class LiteLLMCompletionResponsesConfig:
)
if tool_calls_raw is None:
return []
if isinstance(tool_calls_raw, list):
return tool_calls_raw
tool_calls_items = LiteLLMCompletionResponsesConfig._as_object_list(tool_calls_raw)
if tool_calls_items is not None:
return tool_calls_items
if hasattr(tool_calls_raw, "__iter__") and not isinstance(tool_calls_raw, (str, bytes)):
return list(tool_calls_raw)
return []
@staticmethod
def _check_tool_call_exists(tool_calls: list[Any], tool_call_id: str) -> bool:
def _check_tool_call_exists(tool_calls: Sequence[object], tool_call_id: str) -> bool:
"""Check if a tool_call with the given ID exists in the list."""
for tool_call in tool_calls:
tool_call_id_to_check: str | None = None
@ -617,7 +634,7 @@ class LiteLLMCompletionResponsesConfig:
return False
@staticmethod
def _reconstruct_tool_call_from_tools(tool_call_id: str, tools: list[Any]) -> dict[str, Any] | None:
def _reconstruct_tool_call_from_tools(tool_call_id: str, tools: Sequence[object]) -> dict[str, object] | None:
"""Reconstruct a minimal tool_call definition from tools list."""
for tool in tools:
if isinstance(tool, dict):
@ -635,7 +652,7 @@ class LiteLLMCompletionResponsesConfig:
return None
@staticmethod
def _get_mapping_or_attr_value(obj: Any, key: str, default: Any = None) -> Any:
def _get_mapping_or_attr_value(obj: object, key: str, default: object = None) -> object:
"""
Safely read a field from dict-like or attribute-based objects.
"""
@ -656,13 +673,13 @@ class LiteLLMCompletionResponsesConfig:
@staticmethod
def _create_tool_call_chunk(
tool_use_definition: dict[str, Any], tool_call_id: str, index: int
tool_use_definition: Mapping[str, object], tool_call_id: str, index: int
) -> ChatCompletionToolCallChunk:
"""Create a ChatCompletionToolCallChunk from tool_use_definition."""
function_raw = LiteLLMCompletionResponsesConfig._get_mapping_or_attr_value(tool_use_definition, "function")
function_name_raw = LiteLLMCompletionResponsesConfig._get_mapping_or_attr_value(function_raw, "name")
function_arguments_raw = LiteLLMCompletionResponsesConfig._get_mapping_or_attr_value(function_raw, "arguments")
function: dict[str, Any] = {
function: dict[str, object] = {
"name": function_name_raw or "",
"arguments": function_arguments_raw or "{}",
}
@ -681,7 +698,7 @@ class LiteLLMCompletionResponsesConfig:
)
@staticmethod
def _normalize_tool_use_definition(tool_use_definition: Any, tool_call_id: str) -> dict[str, Any] | None:
def _normalize_tool_use_definition(tool_use_definition: object, tool_call_id: str) -> dict[str, object] | None:
"""
Normalize cached tool_call definitions to a dict-like shape consumed by _create_tool_call_chunk.
"""
@ -689,7 +706,7 @@ class LiteLLMCompletionResponsesConfig:
return None
if isinstance(tool_use_definition, dict):
normalized_definition: dict[str, Any] = dict(tool_use_definition)
normalized_definition: dict[str, object] = dict(tool_use_definition)
else:
tool_use_id_raw = LiteLLMCompletionResponsesConfig._get_mapping_or_attr_value(tool_use_definition, "id")
tool_use_type_raw = LiteLLMCompletionResponsesConfig._get_mapping_or_attr_value(tool_use_definition, "type")
@ -722,20 +739,42 @@ class LiteLLMCompletionResponsesConfig:
return normalized_definition
@staticmethod
def _add_tool_call_to_assistant(assistant_message: Any, tool_call_chunk: ChatCompletionToolCallChunk) -> None:
def _add_tool_call_to_assistant(assistant_message: object, tool_call_chunk: ChatCompletionToolCallChunk) -> None:
"""Add a tool_call to an assistant message."""
if isinstance(assistant_message, dict):
prev_assistant_dict = cast(dict[str, Any], assistant_message)
prev_assistant_dict = cast(dict[str, object], assistant_message)
if "tool_calls" not in prev_assistant_dict:
prev_assistant_dict["tool_calls"] = []
tool_calls_list = prev_assistant_dict["tool_calls"]
if isinstance(tool_calls_list, list):
tool_calls_list = LiteLLMCompletionResponsesConfig._as_object_list(prev_assistant_dict["tool_calls"])
if tool_calls_list is not None:
tool_calls_list.append(tool_call_chunk)
elif hasattr(assistant_message, "tool_calls"):
if assistant_message.tool_calls is None:
assistant_message.tool_calls = []
if isinstance(assistant_message.tool_calls, list):
assistant_message.tool_calls.append(tool_call_chunk)
existing_tool_calls: list[object] | None = LiteLLMCompletionResponsesConfig._as_object_list(
getattr(assistant_message, "tool_calls", None)
)
if existing_tool_calls is not None:
existing_tool_calls.append(tool_call_chunk)
@staticmethod
def _as_object_list(value: object) -> list[object] | None:
"""Return ``value`` typed as a list of objects when it is a list, else ``None``."""
if isinstance(value, list):
return value
return None
@staticmethod
def _require_attr(obj: object, name: str) -> object:
"""Read a required attribute, preserving the ``AttributeError`` raised when it is absent."""
return getattr(obj, name)
@staticmethod
def _as_object_mapping(value: object) -> dict[str, object] | None:
"""Return ``value`` typed as a string-keyed mapping when it is a dict, else ``None``."""
if isinstance(value, dict):
return value
return None
@staticmethod
def _ensure_tool_results_have_corresponding_tool_calls(
@ -746,7 +785,7 @@ class LiteLLMCompletionResponsesConfig:
| ChatCompletionMessageToolCall
| Message
],
tools: list[Any] | None = None,
tools: Sequence[object] | None = None,
) -> list[
AllMessageValues
| GenericChatCompletionMessage
@ -810,7 +849,7 @@ class LiteLLMCompletionResponsesConfig:
# Type-safe way to set tool_call_id on tool message
if isinstance(message, dict):
# Cast to dict to allow setting tool_call_id
message_dict = cast(dict[str, Any], message)
message_dict = cast(dict[str, object], message)
message_dict["tool_call_id"] = tool_call_id
elif hasattr(message, "tool_call_id"):
setattr(message, "tool_call_id", tool_call_id)
@ -862,7 +901,7 @@ class LiteLLMCompletionResponsesConfig:
@staticmethod
def _transform_responses_api_input_item_to_chat_completion_message(
input_item: Any,
input_item: Mapping[str, object],
) -> list[AllMessageValues | GenericChatCompletionMessage | ChatCompletionResponseMessage]:
"""
Transform a Responses API input item into a Chat Completion message
@ -897,9 +936,10 @@ class LiteLLMCompletionResponsesConfig:
# Since guardrails skip None content anyway, we return empty list to exclude it from structured messages
if content is None:
return []
role_value = input_item.get("role")
return [
GenericChatCompletionMessage(
role=input_item.get("role") or "user",
role=str(role_value) if role_value else "user",
content=LiteLLMCompletionResponsesConfig._transform_responses_api_content_to_chat_completion_content(
content
),
@ -907,7 +947,7 @@ class LiteLLMCompletionResponsesConfig:
]
@staticmethod
def _is_input_item_tool_call_output(input_item: Any) -> bool:
def _is_input_item_tool_call_output(input_item: Mapping[str, object]) -> bool:
"""
Check if the input item is a tool call output
"""
@ -920,7 +960,7 @@ class LiteLLMCompletionResponsesConfig:
]
@staticmethod
def _is_input_item_function_call(input_item: Any) -> bool:
def _is_input_item_function_call(input_item: Mapping[str, object]) -> bool:
"""
Check if the input item is a function call or custom tool call.
Both need to be reconstructed as assistant tool_calls for Chat
@ -930,7 +970,7 @@ class LiteLLMCompletionResponsesConfig:
@staticmethod
def _transform_responses_api_tool_call_output_to_chat_completion_message(
tool_call_output: dict[str, Any],
tool_call_output: Mapping[str, object],
) -> list[AllMessageValues | GenericChatCompletionMessage | ChatCompletionResponseMessage]:
"""
ChatCompletionToolMessage is used to indicate the output from a tool call
@ -942,7 +982,7 @@ class LiteLLMCompletionResponsesConfig:
return []
def _normalize_function_call_output_to_tool_content(
output: Any,
output: object,
) -> Any:
"""
Normalize Responses API function_call_output.output into a shape that downstream
@ -964,22 +1004,25 @@ class LiteLLMCompletionResponsesConfig:
return output
# Some adapters represent tool output as a list of "input_*" parts
if isinstance(output, list):
normalized_blocks: list[dict[str, Any]] = []
output_parts = LiteLLMCompletionResponsesConfig._as_object_list(output)
if output_parts is not None:
normalized_blocks: list[dict[str, object]] = []
text_acc: list[str] = []
for part in output:
if not isinstance(part, dict):
for part in output_parts:
part_mapping = LiteLLMCompletionResponsesConfig._as_object_mapping(part)
if part_mapping is None:
continue
part_type = part.get("type")
part_type = part_mapping.get("type")
if part_type in ("input_text", "output_text", "text"):
txt = part.get("text")
txt = part_mapping.get("text")
if isinstance(txt, str) and txt:
text_acc.append(txt)
normalized_blocks.append({"type": "text", "text": txt})
elif part_type in ("input_image", "image_url"):
image_url_val = part.get("image_url") or part.get("url")
if isinstance(image_url_val, dict):
url = image_url_val.get("url")
image_url_val = part_mapping.get("image_url") or part_mapping.get("url")
image_url_mapping = LiteLLMCompletionResponsesConfig._as_object_mapping(image_url_val)
if image_url_mapping is not None:
url = image_url_mapping.get("url")
if isinstance(url, str) and url:
normalized_blocks.append({"type": "image_url", "image_url": {"url": url}})
elif isinstance(image_url_val, str) and image_url_val:
@ -1066,7 +1109,7 @@ class LiteLLMCompletionResponsesConfig:
@staticmethod
def _transform_responses_api_function_call_to_chat_completion_message(
function_call: dict[str, Any],
function_call: Mapping[str, object],
) -> list[AllMessageValues | GenericChatCompletionMessage | ChatCompletionResponseMessage]:
"""
Transform a Responses API function_call into a Chat Completion message with tool calls
@ -1091,11 +1134,13 @@ class LiteLLMCompletionResponsesConfig:
if not raw_arguments and function_call.get("type") == "custom_tool_call":
raw_input = function_call.get("input") or ""
raw_arguments = json.dumps({"content": raw_input}) if raw_input else ""
call_id_value = function_call.get("call_id") or function_call.get("id") or ""
name_value = function_call.get("name") or ""
tool_call = ChatCompletionToolCallChunk(
id=function_call.get("call_id") or function_call.get("id") or "",
id=str(call_id_value),
type="function",
function=ChatCompletionToolCallFunctionChunk(
name=function_call.get("name") or "",
name=str(name_value),
arguments=str(raw_arguments or ""),
),
index=0,
@ -1111,7 +1156,7 @@ class LiteLLMCompletionResponsesConfig:
return [chat_completion_response_message]
@staticmethod
def _resolve_file_id(item: dict[str, Any]) -> str | None:
def _resolve_file_id(item: Mapping[str, object]) -> object:
"""
Return the effective file_id for a Responses API input_file item.
Explicit file_id takes precedence; file_url is used as fallback so
@ -1120,7 +1165,7 @@ class LiteLLMCompletionResponsesConfig:
return item.get("file_id") or item.get("file_url") or None
@staticmethod
def _transform_input_file_item_to_file_item(item: dict[str, Any]) -> dict[str, Any]:
def _transform_input_file_item_to_file_item(item: Mapping[str, object]) -> dict[str, object]:
"""
Transform a Responses API input_file item to a Chat Completion file item
@ -1130,77 +1175,83 @@ class LiteLLMCompletionResponsesConfig:
Returns:
Dictionary with transformed file structure for Chat Completion
"""
file_dict: dict[str, Any] = {}
file_dict: dict[str, object] = {}
file_id = LiteLLMCompletionResponsesConfig._resolve_file_id(item)
if file_id:
file_dict["file_id"] = file_id
if item.get("file_data"):
file_dict["file_data"] = item["file_data"]
new_item: dict[str, Any] = {"type": "file", "file": file_dict}
new_item: dict[str, object] = {"type": "file", "file": file_dict}
if "cache_control" in item:
new_item["cache_control"] = item["cache_control"]
return new_item
@staticmethod
def _transform_input_image_item_to_image_item(
item: dict[str, Any],
item: Mapping[str, object],
) -> ChatCompletionImageObject:
"""
Transform a Responses API input_image item to a Chat Completion image item
"""
image_url_value = item.get("image_url")
detail_value = item.get("detail")
image_url_obj = ChatCompletionImageUrlObject(
url=item.get("image_url") or "", detail=item.get("detail") or "auto"
url=str(image_url_value) if image_url_value else "",
detail=str(detail_value) if detail_value else "auto",
)
return ChatCompletionImageObject(type="image_url", image_url=image_url_obj)
@staticmethod
def _transform_responses_api_content_to_chat_completion_content(
content: Any,
) -> str | list[str | dict[str, Any]]:
content: object,
) -> str | list[str | dict[str, object]]:
"""
Transform a Responses API content into a Chat Completion content
Note: This function should not be called with None content.
Callers should check for None before calling this function.
"""
content_items = LiteLLMCompletionResponsesConfig._as_object_list(content)
if content is None:
# Defensive check: should not happen if callers check first
# Return empty string as fallback to avoid type errors
return ""
elif isinstance(content, str):
return content
elif isinstance(content, list):
content_list: list[str | dict[str, Any]] = []
for item in content:
elif content_items is not None:
content_list: list[str | dict[str, object]] = []
for item in content_items:
item_mapping = LiteLLMCompletionResponsesConfig._as_object_mapping(item)
if isinstance(item, str):
content_list.append(item)
elif isinstance(item, dict):
if item.get("type") == "input_file":
elif item_mapping is not None:
if item_mapping.get("type") == "input_file":
content_list.append(
LiteLLMCompletionResponsesConfig._transform_input_file_item_to_file_item(item)
LiteLLMCompletionResponsesConfig._transform_input_file_item_to_file_item(item_mapping)
)
elif item.get("type") == "input_image":
image_block = dict(
LiteLLMCompletionResponsesConfig._transform_input_image_item_to_image_item(item)
elif item_mapping.get("type") == "input_image":
image_block: dict[str, object] = dict(
LiteLLMCompletionResponsesConfig._transform_input_image_item_to_image_item(item_mapping)
)
if "cache_control" in item:
image_block["cache_control"] = item["cache_control"]
if "cache_control" in item_mapping:
image_block["cache_control"] = item_mapping["cache_control"]
content_list.append(image_block)
else:
# Skip text blocks with None text to avoid downstream errors
text_value = item.get("text")
text_value = item_mapping.get("text")
if text_value is None:
continue
content_block: dict[str, Any] = {
item_type = item_mapping.get("type")
content_block: dict[str, object] = {
"type": LiteLLMCompletionResponsesConfig._get_chat_completion_request_content_type(
item.get("type") or "text"
str(item_type) if item_type else "text"
),
"text": text_value,
}
if "cache_control" in item:
content_block["cache_control"] = item["cache_control"]
if "cache_control" in item_mapping:
content_block["cache_control"] = item_mapping["cache_control"]
content_list.append(content_block)
return content_list
else:
@ -1283,7 +1334,7 @@ class LiteLLMCompletionResponsesConfig:
parameters = dict(typed_tool.get("parameters", {}) or {})
if not parameters or "type" not in parameters:
parameters["type"] = "object"
chat_completion_tool: dict[str, Any] = {
chat_completion_tool: dict[str, object] = {
"type": "function",
"function": {
"name": typed_tool.get("name") or "",
@ -1324,7 +1375,7 @@ class LiteLLMCompletionResponsesConfig:
@staticmethod
def transform_chat_completion_tool_params_to_responses_api_tools(
chat_completion_tools: list[ChatCompletionToolParam | OpenAIMcpServerTool] | None,
) -> list[dict[str, Any]]:
) -> list[dict[str, object]]:
"""
Transform Chat Completion tool params (e.g. from guardrail output) back to
Responses API request tool format. Inverse of
@ -1332,17 +1383,18 @@ class LiteLLMCompletionResponsesConfig:
"""
if chat_completion_tools is None or not chat_completion_tools:
return []
result: list[dict[str, Any]] = []
result: list[dict[str, object]] = []
for tool in chat_completion_tools:
if not isinstance(tool, dict):
result.append(tool) # type: ignore
continue
if tool.get("type") == "function":
fn = cast(dict[str, Any], tool.get("function") or {})
parameters = dict(fn.get("parameters", {}) or {})
fn = cast(dict[str, object], tool.get("function") or {})
fn_parameters = LiteLLMCompletionResponsesConfig._as_object_mapping(fn.get("parameters"))
parameters: dict[str, object] = dict(fn_parameters) if fn_parameters else {}
if not parameters or "type" not in parameters:
parameters["type"] = "object"
responses_tool: dict[str, Any] = {
responses_tool: dict[str, object] = {
"type": "function",
"name": fn.get("name") or "",
"description": fn.get("description") or "",
@ -1496,9 +1548,9 @@ class LiteLLMCompletionResponsesConfig:
@staticmethod
def convert_response_function_tool_call_to_chat_completion_tool_call(
tool_call_item: Any,
tool_call_item: object,
index: int = 0,
) -> dict[str, Any]:
) -> dict[str, object]:
"""
Convert ResponseFunctionToolCall to ChatCompletionToolCallChunk format.
@ -1511,32 +1563,29 @@ class LiteLLMCompletionResponsesConfig:
"""
# Extract provider_specific_fields if present
provider_specific_fields = getattr(tool_call_item, "provider_specific_fields", None)
item_getter = getattr(tool_call_item, "get", None)
if provider_specific_fields and not isinstance(provider_specific_fields, dict):
provider_specific_fields = (
dict(provider_specific_fields) if hasattr(provider_specific_fields, "__dict__") else {}
)
elif hasattr(tool_call_item, "get") and callable(tool_call_item.get): # type: ignore
provider_fields = tool_call_item.get("provider_specific_fields") # type: ignore
elif callable(item_getter):
provider_fields = item_getter("provider_specific_fields")
if provider_fields:
provider_specific_fields = (
provider_fields
if isinstance(provider_fields, dict)
else (
dict(provider_fields) # type: ignore
if hasattr(provider_fields, "__dict__")
else {}
)
else (dict(provider_fields) if hasattr(provider_fields, "__dict__") else {})
)
function_dict: dict[str, Any] = {
"name": tool_call_item.name,
"arguments": tool_call_item.arguments,
function_dict: dict[str, object] = {
"name": LiteLLMCompletionResponsesConfig._require_attr(tool_call_item, "name"),
"arguments": LiteLLMCompletionResponsesConfig._require_attr(tool_call_item, "arguments"),
}
if provider_specific_fields:
function_dict["provider_specific_fields"] = provider_specific_fields
tool_call_dict: dict[str, Any] = {
tool_call_dict: dict[str, object] = {
"id": LiteLLMCompletionResponsesConfig._tool_call_id_from_responses_item(
getattr(tool_call_item, "id", None),
getattr(tool_call_item, "call_id", None),
@ -1553,9 +1602,9 @@ class LiteLLMCompletionResponsesConfig:
@staticmethod
def convert_apply_patch_tool_call_to_chat_completion_tool_call(
tool_call_item: Any,
tool_call_item: ApplyPatchToolCallLike,
index: int = 0,
) -> dict[str, Any]:
) -> dict[str, object]:
"""
Convert ResponseApplyPatchToolCall to ChatCompletionToolCallChunk format.
@ -1573,7 +1622,7 @@ class LiteLLMCompletionResponsesConfig:
import json
operation_dict = tool_call_item.operation.model_dump()
tool_call_dict: dict[str, Any] = {
tool_call_dict: dict[str, object] = {
"id": tool_call_item.call_id,
"function": {
"name": "apply_patch",
@ -1711,7 +1760,7 @@ class LiteLLMCompletionResponsesConfig:
"""
output_items: list = []
for choice in chat_completion_response.choices or []:
message = getattr(choice, "message", None)
message: object = getattr(choice, "message", None)
if not message:
continue
psf = getattr(message, "provider_specific_fields", None)
@ -1783,13 +1832,13 @@ class LiteLLMCompletionResponsesConfig:
"""
image_generation_items: list[OutputImageGenerationCall] = []
images = getattr(choice.message, "images", [])
images: list[ImageURLListItem] | None = getattr(choice.message, "images", [])
if not images:
return image_generation_items
for idx, image_item in enumerate(images):
# Extract base64 from data URL
image_url = image_item.get("image_url", {}).get("url", "")
image_url = image_item.get("image_url", ImageURLObject(url="")).get("url", "")
base64_data = LiteLLMCompletionResponsesConfig._extract_base64_from_data_url(image_url)
if base64_data:
@ -2034,8 +2083,8 @@ class LiteLLMCompletionResponsesConfig:
@staticmethod
def _transform_text_format_to_response_format(
text_param: dict[str, Any] | Any,
) -> dict[str, Any] | None:
text_param: object,
) -> dict[str, object] | None:
"""
Transform Responses API text.format parameter to Chat Completion response_format parameter.
@ -2062,9 +2111,10 @@ class LiteLLMCompletionResponsesConfig:
if not text_param:
return None
if isinstance(text_param, dict):
format_param = text_param.get("format")
if format_param and isinstance(format_param, dict):
text_mapping = LiteLLMCompletionResponsesConfig._as_object_mapping(text_param)
if text_mapping is not None:
format_param = LiteLLMCompletionResponsesConfig._as_object_mapping(text_mapping.get("format"))
if format_param:
format_type = format_param.get("type")
if format_type == "json_schema":

View file

@ -5,11 +5,11 @@ import json
import time
import traceback
import uuid
from collections.abc import Mapping
from collections.abc import Callable, Mapping, Sequence
from datetime import datetime
from functools import lru_cache
from types import MappingProxyType
from typing import Any, Literal
from typing import TYPE_CHECKING, Any, Literal, Protocol
import httpx
from openai._streaming import SSEDecoder
@ -34,6 +34,18 @@ from litellm.types.llms.openai import ResponsesAPIStreamEvents
from litellm.types.utils import CallTypes
from litellm.utils import async_post_call_success_deployment_hook
if TYPE_CHECKING:
from litellm.types.guardrails import PresidioPerRequestConfig
from litellm.types.llms.openai import (
PART_UNION_TYPES,
ContentPartDoneEvent,
ResponseAPIUsage,
ResponseCreatedEvent,
ResponseInProgressEvent,
ResponsesAPIResponse,
ResponsesAPIStreamingResponse,
)
@lru_cache(maxsize=1)
def _get_openai_response_types():
@ -42,7 +54,7 @@ def _get_openai_response_types():
return openai_types
def _log_background_task_failure(task: asyncio.Task[Any], *, task_name: str) -> None:
def _log_background_task_failure(task: asyncio.Task[None], *, task_name: str) -> None:
if task.cancelled():
return
exception = task.exception()
@ -98,6 +110,20 @@ def _error_event_fields(error_obj: object) -> tuple[str, str | None, str | None]
return message, error_type, code
class _StreamCachingHandler(Protocol):
"""Caching-handler surface attached to the logging object, used to cache completed streamed responses."""
original_function: Callable[..., object]
def _should_store_result_in_cache(
self, original_function: Callable[..., object], kwargs: dict[str, object]
) -> bool: ...
@staticmethod
def should_store(handler: _StreamCachingHandler, kwargs: dict[str, object]) -> bool:
return handler._should_store_result_in_cache(original_function=handler.original_function, kwargs=kwargs)
def _status_code_for_error_fields(error_type: str | None, error_code: str | None) -> int:
fields = tuple(field for field in (error_code, error_type) if field is not None)
if any(field.startswith("rate_limit") or field == "insufficient_quota" for field in fields):
@ -131,7 +157,7 @@ class BaseResponsesAPIStreamingIterator:
self.logging_obj = logging_obj
self.finished = False
self.responses_api_provider_config = responses_api_provider_config
self.completed_response: Any | None = None
self.completed_response: ResponsesAPIStreamingResponse | None = None
self.start_time = getattr(logging_obj, "start_time", datetime.now())
self._failure_handled = False # Track if failure handler has been called
self._yielded_first_chunk = False
@ -154,7 +180,7 @@ class BaseResponsesAPIStreamingIterator:
model=model or "",
optional_params=self.logging_obj.model_call_details.get("litellm_params", {}),
)
_model_info: dict = litellm_metadata.get("model_info", {}) if litellm_metadata else {}
_model_info: dict[str, object] = litellm_metadata.get("model_info", {}) if litellm_metadata else {}
self._hidden_params = {
"model_id": _model_info.get("id", None),
"api_base": _api_base,
@ -176,7 +202,7 @@ class BaseResponsesAPIStreamingIterator:
llm_provider=self.custom_llm_provider or "",
)
def _process_chunk(self, chunk) -> Any | None:
def _process_chunk(self, chunk: str) -> ResponsesAPIStreamingResponse | None:
"""Process a single chunk of data from the stream"""
if not chunk:
return None
@ -196,7 +222,7 @@ class BaseResponsesAPIStreamingIterator:
try:
# Parse the JSON chunk
parsed_chunk = json.loads(chunk)
parsed_chunk: object = json.loads(chunk)
# Format as ResponsesAPIStreamingResponse
if isinstance(parsed_chunk, dict):
@ -212,7 +238,7 @@ class BaseResponsesAPIStreamingIterator:
# Using getattr(..., "response") alone is unsafe with Mocks: they synthesize a
# truthy child Mock for any attribute, which breaks tests and is wrong on stream.
if "response" in parsed_chunk:
response_object = getattr(openai_responses_api_chunk, "response", None)
response_object: ResponsesAPIResponse | None = getattr(openai_responses_api_chunk, "response", None)
if response_object is not None:
response = ResponsesAPIRequestUtils._update_responses_api_response_id_with_model_id(
responses_api_response=response_object,
@ -250,7 +276,7 @@ class BaseResponsesAPIStreamingIterator:
model_id=_stream_model_id,
)
elif _event_type == ResponsesAPIStreamEvents.CONTENT_PART_DONE:
_part = getattr(openai_responses_api_chunk, "part", None)
_part: object = getattr(openai_responses_api_chunk, "part", None)
if _part is not None:
if isinstance(_part, dict):
ResponsesAPIRequestUtils._encode_container_ids_in_annotations(
@ -273,7 +299,7 @@ class BaseResponsesAPIStreamingIterator:
openai_types.ResponsesAPIStreamEvents.OUTPUT_ITEM_ADDED,
openai_types.ResponsesAPIStreamEvents.OUTPUT_ITEM_DONE,
):
item = getattr(openai_responses_api_chunk, "item", None)
item: object = getattr(openai_responses_api_chunk, "item", None)
if item:
encrypted_content = getattr(item, "encrypted_content", None)
if encrypted_content and isinstance(encrypted_content, str):
@ -299,9 +325,11 @@ class BaseResponsesAPIStreamingIterator:
self.completed_response = openai_responses_api_chunk
# Add cost to usage object if include_cost_in_streaming_usage is True
if litellm.include_cost_in_streaming_usage and self.logging_obj is not None:
response_obj: Any | None = getattr(openai_responses_api_chunk, "response", None)
response_obj: ResponsesAPIResponse | None = getattr(
openai_responses_api_chunk, "response", None
)
if response_obj:
usage_obj: Any | None = getattr(response_obj, "usage", None)
usage_obj: object | None = getattr(response_obj, "usage", None)
if usage_obj is not None:
try:
cost: float | None = self.logging_obj._response_cost_calculator(result=response_obj)
@ -377,10 +405,10 @@ class BaseResponsesAPIStreamingIterator:
)
self._run_post_success_hooks(end_time=end_time)
def _handle_logging_completed_response(self):
def _handle_logging_completed_response(self) -> None:
"""Base implementation - should be overridden by subclasses"""
def _handle_logging_failed_response(self):
def _handle_logging_failed_response(self) -> None:
"""
Handle logging for RESPONSE_FAILED events by routing to failure handlers.
@ -389,8 +417,10 @@ class BaseResponsesAPIStreamingIterator:
async_failure_handler / failure_handler so logging integrations correctly
record the call as failed.
"""
response_obj = getattr(self.completed_response, "response", None) if self.completed_response else None
error_info = getattr(response_obj, "error", None) if response_obj else None
response_obj: ResponsesAPIResponse | None = (
getattr(self.completed_response, "response", None) if self.completed_response else None
)
error_info: object | None = getattr(response_obj, "error", None) if response_obj else None
error_message, error_type, error_code = _error_event_fields(error_info)
self._record_failed_response_usage(response_obj)
exception = litellm.APIError(
@ -401,10 +431,10 @@ class BaseResponsesAPIStreamingIterator:
)
self._handle_failure(exception)
def _record_failed_response_usage(self, response_obj: Any | None) -> None:
def _record_failed_response_usage(self, response_obj: ResponsesAPIResponse | None) -> None:
if response_obj is None or self.logging_obj is None:
return
usage_obj = getattr(response_obj, "usage", None)
usage_obj: ResponseAPIUsage | None = getattr(response_obj, "usage", None)
if usage_obj is None:
return
try:
@ -451,7 +481,7 @@ class BaseResponsesAPIStreamingIterator:
is_pre_first_chunk=not self._yielded_first_chunk,
)
def _get_completed_response_object(self) -> Any | None:
def _get_completed_response_object(self) -> ResponsesAPIResponse | None:
openai_types = _get_openai_response_types()
completed_response = self.completed_response
if isinstance(completed_response, openai_types.ResponsesAPIResponse):
@ -476,15 +506,15 @@ class BaseResponsesAPIStreamingIterator:
if response_obj is None:
return
caching_handler = getattr(self.logging_obj, "_llm_caching_handler", None)
caching_handler: _StreamCachingHandler | None = getattr(self.logging_obj, "_llm_caching_handler", None)
if caching_handler is None:
return
request_kwargs = getattr(caching_handler, "request_kwargs", None)
request_kwargs: object = getattr(caching_handler, "request_kwargs", None)
if not isinstance(request_kwargs, dict) or request_kwargs.get("stream") is not True:
return
request_kwargs = request_kwargs.copy()
preset_cache_key = getattr(caching_handler, "preset_cache_key", None)
preset_cache_key: object = getattr(caching_handler, "preset_cache_key", None)
request_cache_key = request_kwargs.pop("cache_key", None)
if preset_cache_key is None:
preset_cache_key = request_cache_key
@ -494,10 +524,7 @@ class BaseResponsesAPIStreamingIterator:
if preset_cache_key is not None:
request_kwargs["cache_key"] = preset_cache_key
if not caching_handler._should_store_result_in_cache(
original_function=caching_handler.original_function,
kwargs=request_kwargs,
):
if not _StreamCachingHandler.should_store(caching_handler, request_kwargs):
return
if litellm.cache is None:
@ -527,7 +554,9 @@ class BaseResponsesAPIStreamingIterator:
self._completed_response_cached = True
async def _call_post_streaming_deployment_hook(self, chunk):
async def _call_post_streaming_deployment_hook(
self, chunk: ResponsesAPIStreamingResponse
) -> ResponsesAPIStreamingResponse:
"""
Allow callbacks to modify streaming chunks before returning (parity with chat).
"""
@ -564,7 +593,9 @@ class BaseResponsesAPIStreamingIterator:
except Exception:
return chunk
async def call_post_streaming_hooks_for_testing(self, chunk):
async def call_post_streaming_hooks_for_testing(
self, chunk: ResponsesAPIStreamingResponse
) -> ResponsesAPIStreamingResponse:
"""
Helper to invoke streaming deployment hooks explicitly (used in tests).
"""
@ -666,7 +697,9 @@ class BaseResponsesAPIStreamingIterator:
pass
async def call_post_streaming_hooks_for_testing(iterator, chunk):
async def call_post_streaming_hooks_for_testing(
iterator: BaseResponsesAPIStreamingIterator, chunk: ResponsesAPIStreamingResponse
) -> ResponsesAPIStreamingResponse:
"""
Module-level helper for tests to ensure hooks can be invoked even if the iterator is wrapped.
"""
@ -707,7 +740,7 @@ class ResponsesAPIStreamingIterator(BaseResponsesAPIStreamingIterator):
def __aiter__(self):
return self
async def __anext__(self) -> Any:
async def __anext__(self) -> ResponsesAPIStreamingResponse:
try:
self._check_max_streaming_duration()
while True:
@ -753,7 +786,7 @@ class ResponsesAPIStreamingIterator(BaseResponsesAPIStreamingIterator):
self._handle_failure(e)
raise e
def _handle_logging_completed_response(self):
def _handle_logging_completed_response(self) -> None:
"""Handle logging for completed responses in async context"""
self._log_completed_response(is_async=True)
@ -789,7 +822,7 @@ class SyncResponsesAPIStreamingIterator(BaseResponsesAPIStreamingIterator):
def __iter__(self):
return self
def __next__(self):
def __next__(self) -> ResponsesAPIStreamingResponse:
try:
self._check_max_streaming_duration()
while True:
@ -835,7 +868,7 @@ class SyncResponsesAPIStreamingIterator(BaseResponsesAPIStreamingIterator):
self._handle_failure(e)
raise e
def _handle_logging_completed_response(self):
def _handle_logging_completed_response(self) -> None:
"""Handle logging for completed responses in sync context"""
self._log_completed_response(is_async=False)
@ -880,7 +913,7 @@ class MockResponsesAPIStreamingIterator(BaseResponsesAPIStreamingIterator):
def _set_events_from_response(
self,
transformed: Any,
transformed: ResponsesAPIResponse,
logging_obj: LiteLLMLoggingObj,
) -> None:
self._events = _build_synthetic_response_events(
@ -894,7 +927,7 @@ class MockResponsesAPIStreamingIterator(BaseResponsesAPIStreamingIterator):
def __aiter__(self):
return self
async def __anext__(self) -> Any:
async def __anext__(self) -> ResponsesAPIStreamingResponse:
if self._idx >= len(self._events):
raise StopAsyncIteration
evt = self._events[self._idx]
@ -908,7 +941,7 @@ class MockResponsesAPIStreamingIterator(BaseResponsesAPIStreamingIterator):
def __iter__(self):
return self
def __next__(self) -> Any:
def __next__(self) -> ResponsesAPIStreamingResponse:
if self._idx >= len(self._events):
raise StopIteration
evt = self._events[self._idx]
@ -923,7 +956,7 @@ class MockResponsesAPIStreamingIterator(BaseResponsesAPIStreamingIterator):
class CachedResponsesAPIStreamingIterator(BaseResponsesAPIStreamingIterator):
def __init__(
self,
response: Any,
response: ResponsesAPIResponse,
logging_obj: LiteLLMLoggingObj,
request_data: dict[str, Any] | None = None,
call_type: str | None = None,
@ -941,13 +974,13 @@ class CachedResponsesAPIStreamingIterator(BaseResponsesAPIStreamingIterator):
)
self._completed_response_cache_hit = True
self._persist_completed_response_before_logging = False
self._events: list[Any] = []
self._events: list[ResponsesAPIStreamingResponse] = []
self._idx = 0
self._set_events_from_response(transformed=response, logging_obj=logging_obj)
def _set_events_from_response(
self,
transformed: Any,
transformed: ResponsesAPIResponse,
logging_obj: LiteLLMLoggingObj,
) -> None:
self._events = _build_synthetic_response_events(
@ -961,7 +994,7 @@ class CachedResponsesAPIStreamingIterator(BaseResponsesAPIStreamingIterator):
def __aiter__(self):
return self
async def __anext__(self) -> Any:
async def __anext__(self) -> ResponsesAPIStreamingResponse:
if self._idx >= len(self._events):
raise StopAsyncIteration
evt = self._events[self._idx]
@ -975,7 +1008,7 @@ class CachedResponsesAPIStreamingIterator(BaseResponsesAPIStreamingIterator):
def __iter__(self):
return self
def __next__(self) -> Any:
def __next__(self) -> ResponsesAPIStreamingResponse:
if self._idx >= len(self._events):
raise StopIteration
evt = self._events[self._idx]
@ -997,11 +1030,11 @@ def _dump_response_object(obj: Any) -> dict[str, Any]:
def _build_response_status_event(
event_type: Literal[
"response.created",
"response.in_progress",
ResponsesAPIStreamEvents.RESPONSE_CREATED,
ResponsesAPIStreamEvents.RESPONSE_IN_PROGRESS,
],
transformed: Any,
) -> Any:
transformed: ResponsesAPIResponse,
) -> ResponseCreatedEvent | ResponseInProgressEvent:
openai_types = _get_openai_response_types()
in_progress_response = transformed.model_copy(
deep=True,
@ -1018,10 +1051,10 @@ def _build_content_part_done_event(
output_index: int,
content_index: int,
part_payload: dict[str, Any],
) -> Any | None:
) -> ContentPartDoneEvent | None:
openai_types = _get_openai_response_types()
part_type = part_payload.get("type")
part: Any
part: PART_UNION_TYPES
if part_type == "output_text":
annotations = [
openai_types.BaseLiteLLMOpenAIResponseObject(**annotation)
@ -1057,7 +1090,7 @@ def _build_content_part_done_event(
def _add_text_like_part_events(
*,
events: list[Any],
events: list[ResponsesAPIStreamingResponse],
item_id: str,
output_index: int,
content_index: int,
@ -1123,13 +1156,13 @@ def _add_text_like_part_events(
def _build_synthetic_response_events(
*,
transformed: Any,
transformed: ResponsesAPIResponse,
logging_obj: LiteLLMLoggingObj,
chunk_size: int,
) -> list[Any]:
) -> list[ResponsesAPIStreamingResponse]:
openai_types = _get_openai_response_types()
if litellm.include_cost_in_streaming_usage and logging_obj is not None:
usage_obj: Any | None = getattr(transformed, "usage", None)
usage_obj: object | None = getattr(transformed, "usage", None)
if usage_obj is not None:
try:
cost: float | None = logging_obj._response_cost_calculator(result=transformed)
@ -1138,13 +1171,14 @@ def _build_synthetic_response_events(
except Exception:
pass
events: list[Any] = [
events: list[ResponsesAPIStreamingResponse] = [
_build_response_status_event(openai_types.ResponsesAPIStreamEvents.RESPONSE_CREATED, transformed),
_build_response_status_event(openai_types.ResponsesAPIStreamEvents.RESPONSE_IN_PROGRESS, transformed),
]
sequence_number = 0
for output_index, output_item in enumerate(getattr(transformed, "output", []) or []):
output_items: Sequence[object] = getattr(transformed, "output", []) or []
for output_index, output_item in enumerate(output_items):
output_item_payload = _dump_response_object(output_item)
item_id = str(output_item_payload.get("id") or transformed.id)
item_type = output_item_payload.get("type")
@ -1158,7 +1192,8 @@ def _build_synthetic_response_events(
)
if item_type == "message":
for content_index, part in enumerate(output_item_payload.get("content", []) or []):
content_payload: Sequence[object] = output_item_payload.get("content", []) or []
for content_index, part in enumerate(content_payload):
part_payload = _dump_response_object(part)
events.append(
openai_types.ContentPartAddedEvent(
@ -1205,7 +1240,8 @@ def _build_synthetic_response_events(
)
)
elif item_type == "reasoning":
for summary_index, summary in enumerate(output_item_payload.get("summary", []) or []):
summary_payload_items: Sequence[object] = output_item_payload.get("summary", []) or []
for summary_index, summary in enumerate(summary_payload_items):
summary_payload = _dump_response_object(summary)
summary_text = str(summary_payload.get("text") or "")
for i in range(0, len(summary_text), chunk_size):
@ -1277,6 +1313,46 @@ RESPONSES_WS_LOGGED_EVENT_TYPES = [
RESPONSES_WS_MASKABLE_TEXT_BLOCK_TYPES = frozenset({"input_text", "output_text", "text"})
class _ClientWebSocket(Protocol):
"""Client-facing socket surface used by the Responses WebSocket handlers."""
async def send_text(self, data: str) -> None: ...
async def receive_text(self) -> str: ...
class _BackendWebSocket(Protocol):
"""Upstream provider socket surface used by the Responses WebSocket handlers."""
async def recv(self, decode: bool = ...) -> str | bytes: ...
async def send(self, message: str) -> None: ...
async def close(self) -> None: ...
class _PIIMaskingGuardrail(Protocol):
"""Guardrail surface used for Presidio PII masking/unmasking of WebSocket frames."""
def get_presidio_settings_from_request_data(
self, data: Mapping[str, object]
) -> PresidioPerRequestConfig | None: ...
async def check_pii(
self,
text: str,
output_parse_pii: bool,
presidio_config: PresidioPerRequestConfig | None,
request_data: Mapping[str, object],
) -> str: ...
def _unmask_pii_text(self, text: str, pii_tokens: Mapping[str, str]) -> str: ...
@staticmethod
def unmask(guardrail: _PIIMaskingGuardrail, text: str, pii_tokens: Mapping[str, str]) -> str:
return guardrail._unmask_pii_text(text, pii_tokens)
class ResponsesWebSocketStreaming:
"""
Manages bidirectional WebSocket forwarding for the Responses API
@ -1292,31 +1368,31 @@ class ResponsesWebSocketStreaming:
def __init__(
self,
websocket: Any,
backend_ws: Any,
websocket: _ClientWebSocket,
backend_ws: _BackendWebSocket,
logging_obj: LiteLLMLoggingObj,
user_api_key_dict: Any | None = None,
request_data: dict | None = None,
user_api_key_dict: object | None = None,
request_data: dict[str, object] | None = None,
first_message: str | None = None,
guardrail_callbacks: list[Any] | None = None,
output_guardrail_callbacks: list[Any] | None = None,
guardrail_callbacks: list[_PIIMaskingGuardrail] | None = None,
output_guardrail_callbacks: list[_PIIMaskingGuardrail] | None = None,
authorized_model: str | None = None,
):
self.websocket = websocket
self.backend_ws = backend_ws
self.logging_obj = logging_obj
self.user_api_key_dict = user_api_key_dict
self.request_data: dict = request_data or {}
self.messages: list[dict] = []
self.request_data: dict[str, object] = request_data or {}
self.messages: list[dict[str, object]] = []
self.input_messages: list[dict[str, str]] = []
self.first_message = first_message
self.guardrail_callbacks: list[Any] = guardrail_callbacks or []
self.output_guardrail_callbacks: list[Any] = output_guardrail_callbacks or []
self.guardrail_callbacks: list[_PIIMaskingGuardrail] = guardrail_callbacks or []
self.output_guardrail_callbacks: list[_PIIMaskingGuardrail] = output_guardrail_callbacks or []
# Model name authorized at connection time; enforced on every
# response.create frame to prevent deployment-substitution attacks.
self.authorized_model: str | None = authorized_model
def _should_store_event(self, event_obj: dict) -> bool:
def _should_store_event(self, event_obj: dict[str, object]) -> bool:
return event_obj.get("type") in RESPONSES_WS_LOGGED_EVENT_TYPES
def _store_event(self, event: Any) -> None:
@ -1333,7 +1409,7 @@ class ResponsesWebSocketStreaming:
if self._should_store_event(event_obj):
self.messages.append(event_obj)
def _collect_input_from_client_event(self, message: Any) -> None:
def _collect_input_from_client_event(self, message: object) -> None:
"""Extract user input content from response.create for logging."""
try:
if isinstance(message, str):
@ -1368,7 +1444,7 @@ class ResponsesWebSocketStreaming:
except (json.JSONDecodeError, AttributeError, TypeError):
pass
def _store_input(self, message: Any) -> None:
def _store_input(self, message: object) -> None:
self._collect_input_from_client_event(message)
if self.logging_obj:
self.logging_obj.pre_call(input=message, api_key="")
@ -1429,7 +1505,7 @@ class ResponsesWebSocketStreaming:
finally:
await self._log_messages()
def _enforce_authorized_model(self, msg_obj: dict) -> bool:
def _enforce_authorized_model(self, msg_obj: dict[str, object]) -> bool:
"""
Overwrite any ``model`` field in a ``response.create`` frame with the
connection-authorized model to prevent deployment-substitution attacks.
@ -1495,8 +1571,9 @@ class ResponsesWebSocketStreaming:
# nested: {"type": "response.create", "response": {"input": ..., "instructions": ...}}
# Mask "input" and "instructions" in both shapes so PII is never
# forwarded unmasked regardless of where the client places it.
nested_response = msg_obj.get("response") if isinstance(msg_obj.get("response"), dict) else None
text_containers: list[tuple[dict, str]] = []
raw_nested_response = msg_obj.get("response")
nested_response = raw_nested_response if isinstance(raw_nested_response, dict) else None
text_containers: list[tuple[dict[str, object], str]] = []
for container in (msg_obj, nested_response):
if container is None:
continue
@ -1590,7 +1667,8 @@ class ResponsesWebSocketStreaming:
if not self.guardrail_callbacks:
return response_str
pii_tokens: dict[str, str] = (self.request_data.get("metadata") or {}).get("pii_tokens", {})
metadata = self.request_data.get("metadata")
pii_tokens: dict[str, str] = metadata.get("pii_tokens", {}) if isinstance(metadata, dict) else {}
if not pii_tokens:
return response_str
@ -1618,7 +1696,7 @@ class ResponsesWebSocketStreaming:
continue
text = content_block.get("text")
if isinstance(text, str):
unmasked = cb._unmask_pii_text(text, pii_tokens)
unmasked = _PIIMaskingGuardrail.unmask(cb, text, pii_tokens)
if unmasked != text:
content_block["text"] = unmasked
modified = True
@ -1627,7 +1705,7 @@ class ResponsesWebSocketStreaming:
if event_type in self._DELTA_EVENT_TYPES:
delta = evt_obj.get("delta")
if isinstance(delta, str):
unmasked = cb._unmask_pii_text(delta, pii_tokens)
unmasked = _PIIMaskingGuardrail.unmask(cb, delta, pii_tokens)
if unmasked != delta:
evt_obj["delta"] = unmasked
return json.dumps(evt_obj)
@ -1650,7 +1728,7 @@ class ResponsesWebSocketStreaming:
return response_str
try:
evt_obj = json.loads(response_str)
evt_obj: Mapping[str, object] = json.loads(response_str)
except (json.JSONDecodeError, TypeError):
return response_str
@ -1756,12 +1834,12 @@ class ResponsesWebSocketStreaming:
# Managed WebSocket mode (HTTP-backed, provider-agnostic)
# ---------------------------------------------------------------------------
_RESPONSE_CREATE_PARAMS: frozenset = (
_RESPONSE_CREATE_PARAMS: frozenset[str] = (
_get_openai_response_types().ResponsesAPIRequestParams.__required_keys__
| _get_openai_response_types().ResponsesAPIRequestParams.__optional_keys__
)
_MANAGED_WS_SKIP_KWARGS: frozenset = frozenset(
_MANAGED_WS_SKIP_KWARGS: frozenset[str] = frozenset(
{
"litellm_logging_obj",
"litellm_call_id",
@ -1793,10 +1871,10 @@ class ManagedResponsesWebSocketHandler:
def __init__(
self,
websocket: Any,
websocket: _ClientWebSocket,
model: str,
logging_obj: LiteLLMLoggingObj,
user_api_key_dict: Any | None = None,
user_api_key_dict: object | None = None,
litellm_metadata: dict[str, Any] | None = None,
api_key: str | None = None,
api_base: str | None = None,

View file

@ -1,8 +1,115 @@
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Mapping, Optional, Protocol, Sequence, Union
from typing_extensions import TypedDict
from typing_extensions import Required, TypedDict
from ..utils import CompletionTokensDetails, PromptTokensDetailsWrapper, ServerToolUse
from ..llms.openai import ChatCompletionAudioDelta
from ..utils import (
CompletionTokensDetails,
CompletionTokensDetailsWrapper,
Function,
PromptTokensDetailsWrapper,
ServerToolUse,
Usage,
)
class StreamingUsageDict(TypedDict, total=False):
prompt_tokens: Optional[int]
completion_tokens: Optional[int]
total_tokens: Optional[int]
reasoning_tokens: Optional[int]
prompt_tokens_details: Union[PromptTokensDetailsWrapper, dict[str, object], None]
completion_tokens_details: Union[CompletionTokensDetailsWrapper, dict[str, object], None]
server_tool_use: Union[ServerToolUse, dict[str, object], None]
cost: Optional[float]
cache_creation_input_tokens: Optional[int]
cache_read_input_tokens: Optional[int]
class StreamingToolCallFunctionDict(TypedDict, total=False):
name: Optional[str]
arguments: Optional[str]
provider_specific_fields: Optional[Mapping[str, object]]
class StreamingToolCallFunctionLike(Protocol):
name: Optional[str]
arguments: Optional[str]
provider_specific_fields: Optional[Mapping[str, object]]
class StreamingToolCallDict(TypedDict, total=False):
id: Optional[str]
type: Optional[str]
index: int
function: Union[StreamingToolCallFunctionDict, StreamingToolCallFunctionLike, None]
provider_specific_fields: Optional[Mapping[str, object]]
class StreamingToolCallLike(Protocol):
id: Optional[str]
type: Optional[str]
index: int
function: Optional[StreamingToolCallFunctionLike]
provider_specific_fields: Optional[Mapping[str, object]]
class StreamingThinkingBlockDict(TypedDict, total=False):
type: Optional[str]
thinking: Optional[str]
data: Optional[str]
signature: Optional[str]
class StreamingChunkDelta(TypedDict, total=False):
role: Required[Optional[str]]
content: Optional[str]
reasoning_content: Optional[str]
tool_calls: Sequence[Union[StreamingToolCallDict, StreamingToolCallLike]]
thinking_blocks: Optional[Sequence[StreamingThinkingBlockDict]]
audio: Optional[ChatCompletionAudioDelta]
class StreamingChunkChoice(TypedDict, total=False):
index: int
finish_reason: Optional[str]
delta: Required[StreamingChunkDelta]
class StreamingChunkDict(TypedDict, total=False):
_hidden_params: dict[str, object]
id: Required[str]
object: Required[str]
created: Required[int]
model: Required[str]
system_fingerprint: Optional[str]
choices: Required[list[StreamingChunkChoice]]
usage: Union[Usage, StreamingUsageDict, None]
class UsageChunkCalculation(TypedDict):
prompt_tokens: Optional[int]
completion_tokens: Optional[int]
cache_creation_input_tokens: Optional[int]
cache_read_input_tokens: Optional[int]
completion_tokens_details: Optional[CompletionTokensDetails]
prompt_tokens_details: Optional[PromptTokensDetailsWrapper]
cost: Optional[float]
class ToolCallAccumulator(TypedDict):
id: Optional[str]
name: Optional[str]
type: Optional[str]
arguments: list[str]
provider_specific_fields: Optional[dict[str, object]]
class ToolCallParams(TypedDict, total=False):
id: str
function: Function
type: str
provider_specific_fields: dict[str, object]
class UsagePerChunk(TypedDict):

View file

@ -0,0 +1,75 @@
"""
Types for passthrough managed-ID rewriting.
The Prisma client is an untyped runtime wrapper, so the managed-file /
managed-object rows and the table actions read by
``litellm.proxy.pass_through_endpoints.managed_id_rewriter`` are described
structurally here instead of being imported from generated stubs.
"""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from datetime import datetime
from typing import Protocol, TypedDict
PrismaWhere = dict[str, object]
PrismaOrder = dict[str, str] | list[dict[str, str]]
class ManagedResourceOwner(Protocol):
"""Ownership columns carried by every managed-resource row."""
@property
def created_by(self) -> str | None: ...
@property
def team_id(self) -> str | None: ...
class ManagedResourceRow(ManagedResourceOwner, Protocol):
"""A ``LiteLLM_ManagedFileTable`` or ``LiteLLM_ManagedObjectTable`` row.
``unified_file_id`` exists only on file rows and ``unified_object_id`` only
on object rows; each is read exclusively off the table it belongs to.
"""
@property
def created_at(self) -> datetime | None: ...
@property
def file_object(self) -> object: ...
@property
def unified_file_id(self) -> str: ...
@property
def unified_object_id(self) -> str: ...
class ManagedResourceTable(Protocol):
"""The Prisma table actions the managed-ID rewriter issues."""
async def find_first(self, *, where: PrismaWhere) -> ManagedResourceRow | None: ...
async def find_many(
self,
*,
where: PrismaWhere,
order: PrismaOrder | None = None,
take: int | None = None,
) -> Sequence[ManagedResourceRow]: ...
async def update(self, *, where: PrismaWhere, data: PrismaWhere) -> ManagedResourceRow | None: ...
async def upsert(self, *, where: PrismaWhere, data: Mapping[str, PrismaWhere]) -> ManagedResourceRow: ...
class PassthroughListResponse(TypedDict):
"""OpenAI-style paginated list body served from the managed-ID tables."""
object: str
data: Sequence[Mapping[str, object]]
first_id: str | None
last_id: str | None
has_more: bool

View file

@ -1,10 +1,19 @@
from typing import Dict, List, Union, Any, Optional
from typing import Dict, List, TypedDict, Union, Any, Optional
from pydantic import BaseModel, Field
from ...router import ModelGroupInfo
class PrismaCompatibleCreateDBModel(TypedDict):
model_id: Optional[str]
model_name: str
litellm_params: str
model_info: str
created_by: str
updated_by: str
class ModelGroupInfoProxy(ModelGroupInfo):
is_public_model_group: bool = Field(default=False)
health_status: Optional[str] = Field(default=None)

View file

@ -1,7 +1,7 @@
from typing import List, Literal, Optional, Union
from typing import List, Literal, Optional, Protocol, Union
from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall
from pydantic import PrivateAttr
from pydantic import BaseModel, PrivateAttr
from typing_extensions import Any, List, Optional, TypedDict
from litellm.types.llms.base import BaseLiteLLMOpenAIResponseObject
@ -140,3 +140,13 @@ class DecodedResponseId(TypedDict, total=False):
custom_llm_provider: Optional[str]
model_id: Optional[str]
response_id: str
class ApplyPatchToolCallLike(Protocol):
"""Structural view of an openai ``ResponseApplyPatchToolCall`` used by the completion bridge."""
@property
def call_id(self) -> str: ...
@property
def operation(self) -> BaseModel: ...

View file

@ -1,6 +1,6 @@
{
"ANN001": {
"limit": 3104
"limit": 3099
},
"ANN002": {
"limit": 69
@ -9,13 +9,13 @@
"limit": 831
},
"ANN201": {
"limit": 2137
"limit": 2135
},
"ANN202": {
"limit": 941
"limit": 935
},
"ANN204": {
"limit": 724
"limit": 723
},
"ANN205": {
"limit": 127
@ -24,7 +24,7 @@
"limit": 130
},
"ANN401": {
"limit": 1851
"limit": 1762
},
"ASYNC230": {
"limit": 14

View file

@ -1,9 +1,9 @@
{
"LIT001": {
"limit": 23350
"limit": 23334
},
"LIT002": {
"limit": 27256
"limit": 27251
},
"LIT003": {
"limit": 292
@ -15,7 +15,7 @@
"limit": 0
},
"LIT006": {
"limit": 1105
"limit": 1097
},
"LIT007": {
"limit": 0
@ -24,6 +24,6 @@
"limit": 1004
},
"LIT009": {
"limit": 2465
"limit": 2458
}
}