refactor(lint): retire LIT002 and allow dict annotations in LIT001

LIT002 banned every list/dict/set literal, comprehension and constructor call. In
practice it produced # mutable-ok on most lines that touched a library, pushed
defensive deep copies of self-referential objects (a memory leak), and forced
MappingProxyType <-> dict conversions at every boundary. LIT001 keeps mutable
sequences and sets out of annotations; dict/Dict/defaultdict/MutableMapping are
now allowed. Dropped the 1430 mutable-ok comments no remaining rule needs.
type-discipline-budget.json is left for the ratchet automation.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
mateo 2026-09-14 18:22:59 +00:00
parent 08a78a3982
commit 0246769138
284 changed files with 1479 additions and 2254 deletions

View file

@ -60,7 +60,7 @@ Never edit or commit `ruff-strict-budget.json`, `type-discipline-budget.json`, `
If you're trying to create a new function that relies on untyped stuff, instead of adding more Any's and pushing `reportAny` / `reportExplicitAny` closer to their basedpyright ceilings, just validate it in the caller with Pydantic (a model or `TypeAdapter` that returns the typed thing or raises will do) and then pass the now typed variable in
If you get an LIT001 or LIT002 fail, refactor the code to follow functional programming best practices rather than introducing mutable data structures. For example, build values in one shot with comprehensions or generators wrapped in `tuple()` / `MappingProxyType()` / `frozenset()` instead of seeding an empty `list`/`dict`/`set` and mutating it over time. Ideally, `# mutable-ok` is never used; reach for it only as a genuine last resort when an immutable rewrite is truly impossible, and always pair it with a real reason
If you get an LIT001 fail, refactor the code to follow functional programming best practices rather than introducing mutable sequences or sets. For example, build values in one shot with comprehensions or generators wrapped in `tuple()` / `frozenset()` instead of seeding an empty `list`/`set` and appending to it over time. Ideally, `# mutable-ok` is never used; reach for it only as a last resort when an immutable rewrite is impossible, and always pair it with a real reason. Plain `dict` is allowed: most of the Python ecosystem takes and returns dicts, and converting to `MappingProxyType` at every boundary costs more than it protects. Never deep copy a value just to hand out an immutable view; a defensive copy of a self-referential or large object is worse than the mutation it guards against
Every lint or type suppression must name the exact rule inside brackets and carry a reason comment, e.g. `# pyright: ignore[reportArgumentType] # stubs lack async overload` or `# noqa: TID251 # <reason>`. `# type: ignore` is banned (LIT009): pyrightconfig.json sets `enableTypeIgnoreComments` to false, so it silently does nothing
@ -91,7 +91,7 @@ Follow these coding conventions for new/updated code (a three-line fix in a lega
- Composition over inheritance
- Never-nester: early returns over deep nesting
- Don't throw; model failures as values (One function (e.g., raise_public) maps error union to existing public exception contracts via exhaustive match + assert_never)
- No mutation; don't reassign variables, global or local. Instead of mutable lists and dicts, prefer tuples, frozen dataclasses (with slots=True), `MappingProxyType`, etc.
- No mutation; don't reassign variables, global or local. Prefer tuples over lists and frozensets over sets. For structured records prefer frozen dataclasses (with slots=True) or `ReadOnly` TypedDicts over dicts. A `dict` is fine for a mapping with arbitrary keys, especially at a library boundary; `MappingProxyType` is optional there, and never worth a copy
- Annotate every variable with `: Final` (LIT010). Unpacking and walrus targets cannot carry the annotation, so they are implicitly final. Don't rebind them. Never rebind or mutate function parameters (LIT011); `self`/`cls` attribute stores are the exception. If rebinding or in-place mutation is truly unavoidable, suppress with `# rebind-ok: <reason>`
- Qualify every TypedDict field with `ReadOnly[...]` (LIT012), which nests freely with `Required` / `NotRequired` / `Annotated` in any order. If making the key writable is truly unavoidable, suppress with `# writable-ok: <reason>`
- Use dependency injection

View file

@ -616,7 +616,7 @@ class _ENTERPRISE_SecretDetection(CustomGuardrail):
data["prompt"] = self.redact_text(prompt, source="prompt")
return 1
if isinstance(prompt, list):
data["prompt"] = [ # mutable-ok: data["prompt"] is a list on the wire
data["prompt"] = [
self.redact_text(item, source="prompt")
if isinstance(item, str) and item
else item

View file

@ -203,7 +203,7 @@ def _str_to_bool(value: str) -> bool:
def _coerce_redis_kwargs_types(
redis_kwargs: Mapping[str, object],
client: type | tuple[type, ...] = redis.Redis,
) -> dict[str, object]: # mutable-ok: a caller mutates the returned kwargs before constructing its client
) -> dict[str, object]:
"""Coerces string values to the numeric/boolean type ``client``'s constructor
declares for that parameter. ``client`` may be a tuple of client classes; a
parameter's type is taken from the first signature that declares it, which
@ -233,7 +233,7 @@ def _coerce_redis_kwargs_types(
"socket_keepalive": bool,
}
)
result: Final = dict(redis_kwargs) # mutable-ok: per-key try/except coercion below needs to drop individual keys
result: Final = dict(redis_kwargs)
for key, value in redis_kwargs.items():
if not isinstance(value, str):
continue
@ -803,7 +803,7 @@ def _async_auth_kwargs(redis_kwargs: dict) -> dict:
superseded: Final = frozenset({"redis_connect_func", "username", "password"})
kept: Final = ((k, v) for k, v in redis_kwargs.items() if k not in superseded)
return dict(kept, credential_provider=credential_provider) # mutable-ok: the branches below mutate these kwargs
return dict(kept, credential_provider=credential_provider)
def get_redis_client(**env_overrides):

View file

@ -110,7 +110,7 @@ async def _handle_completed_batch(
return BatchCostUsageResult(
cost=0.0,
usage=Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
models=[], # mutable-ok: no output file means no model was ever priced; BatchCostUsageResult.models requires list[str]
models=[],
successful_requests=0,
failed_requests=await count_error_file_failed_requests(
batch, custom_llm_provider=custom_llm_provider, litellm_params=litellm_params

View file

@ -222,9 +222,7 @@ class DualCache(BaseCache):
if value is not None:
self.in_memory_cache.set_cache(key, value, **self._backfill_kwargs(kwargs))
return list( # mutable-ok: public list contract
redis_result.get(key) if value is None else value for key, value in zip(keys, result)
)
return list(redis_result.get(key) if value is None else value for key, value in zip(keys, result))
except Exception as e:
log_redis_failure(
verbose_logger, logging.ERROR, "LiteLLM Cache: exception in batch_get_cache", e, with_traceback=True

View file

@ -236,7 +236,7 @@ class EvictedClientCloser:
the front rather than having to be searched for.
"""
with self._queue_lock:
bucket: Final = self._buckets.setdefault(_bucket_key(pending), deque()) # mutable-ok: FIFO by design
bucket: Final = self._buckets.setdefault(_bucket_key(pending), deque())
while bucket and bucket[0].client_ref() is None:
bucket.popleft()
self._pending_count -= 1

View file

@ -142,9 +142,7 @@ def get_litellm_async_redis_cluster_class( # noqa: C901 # supports redis-py ve
self._litellm_reinit_requests = 0
self._litellm_tolerated_timeouts = 0
super().__init__(*args, **kwargs)
self._litellm_consecutive_timeouts: dict[ # mutable-ok: per-node counter updated on the command hot path
str, int
] = {}
self._litellm_consecutive_timeouts: dict[str, int] = {}
@property
def _initialize(self) -> bool:

View file

@ -124,13 +124,13 @@ def _reasoning_input_items(msg: "AllMessageValues") -> list[dict[str, object]]:
blocks are the fallback for turns that arrived over another API surface.
"""
items: Final = _get_reasoning_items(msg)
stored: Final = [_reasoning_item_to_response_input(item) for item in items] # mutable-ok: API message payload
stored: Final = [_reasoning_item_to_response_input(item) for item in items]
if stored:
return stored
raw_blocks: Final = msg.get("thinking_blocks") or ()
blocks: Final = cast("Iterable[ChatCompletionThinkingBlock]", raw_blocks) # cast-ok: untyped client json
replayed: Final = responses_reasoning_items_from_thinking_blocks(blocks)
return [dict(item) for item in replayed] # mutable-ok: API message payload
return [dict(item) for item in replayed]
def _build_reasoning_item(
@ -439,7 +439,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
input_items.extend(_reasoning_input_items(msg))
if content:
input_items.append(
{ # mutable-ok: API message payload
{
"type": "message",
"role": "assistant",
"content": self._convert_content_to_responses_format(content, "assistant"),
@ -473,7 +473,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
if role == "assistant":
input_items.extend(_reasoning_input_items(msg))
input_items.append(
{ # mutable-ok: API message payload
{
"type": "message",
"role": role,
"content": self._convert_content_to_responses_format(content, cast(str, role)),
@ -1311,7 +1311,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
):
super().__init__(streaming_response, sync_stream, json_mode)
self._chat_completion_id: str | None = None
self._tool_call_index_map: dict[int, int] = {} # mutable-ok: per-stream accumulator state
self._tool_call_index_map: dict[int, int] = {}
def _handle_string_chunk(
self, str_line: Union[str, "BaseModel"]
@ -1332,7 +1332,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
@staticmethod
def _sequential_tool_call_index(
tool_call_index_map: dict[int, int] | None, # mutable-ok: per-stream state, remapped in place
tool_call_index_map: dict[int, int] | None,
output_index: int,
) -> int:
"""Chat-completions tool_call indices must be 0-based and sequential, but
@ -1345,13 +1345,13 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
if tool_call_index_map is None:
return output_index
if output_index not in tool_call_index_map:
tool_call_index_map[output_index] = len(tool_call_index_map) # mutable-ok: per-stream accumulator state
tool_call_index_map[output_index] = len(tool_call_index_map)
return tool_call_index_map[output_index]
@staticmethod
def translate_responses_chunk_to_openai_stream(
parsed_chunk: dict | BaseModel,
tool_call_index_map: dict[int, int] | None = None, # mutable-ok: per-stream state, remapped in place
tool_call_index_map: dict[int, int] | None = None,
) -> "ModelResponseStream":
"""
Translate a Responses API streaming chunk to OpenAI chat completion streaming format.
@ -1482,7 +1482,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
# tool call; per-stream callers already received it via
# output_item.added and the argument delta events
return ModelResponseStream(
choices=[ # mutable-ok: ModelResponseStream coerces only list choices
choices=[
StreamingChoices(
index=0,
delta=Delta(
@ -1587,7 +1587,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
)
],
usage=usage,
provider_specific_fields=dict(provider_metadata) or None, # mutable-ok: field is typed dict
provider_specific_fields=dict(provider_metadata) or None,
)
else:
pass

View file

@ -2552,9 +2552,7 @@ class ResponsesWebSocketTokenUsageProcessor(BaseTokenUsageProcessor):
collected_usage_objects: Final = ResponsesWebSocketTokenUsageProcessor.collect_usage_from_responses_ws_results(
results
)
return ResponsesWebSocketTokenUsageProcessor.combine_usage_objects(
list(collected_usage_objects) # mutable-ok: combine_usage_objects requires a list parameter
)
return ResponsesWebSocketTokenUsageProcessor.combine_usage_objects(list(collected_usage_objects))
_TRANSCRIPTION_COMPLETED_EVENT_TYPE: Final = "conversation.item.input_audio_transcription.completed"

View file

@ -653,7 +653,7 @@ class MCPClient:
follow_redirects=True,
event_hooks=MappingProxyType(
{"response": [capture_upstream_error_response], "request": [guard] if guard else []}
), # mutable-ok: httpx types require lists of hooks
),
)
return factory

View file

@ -171,9 +171,7 @@ async def load_mcp_tools(
"""
tools: Final = await list_tools_with_pagination(session)
if format == "openai":
return [ # mutable-ok: public API returns a list
transform_mcp_tool_to_openai_tool(mcp_tool=tool) for tool in tools
]
return [transform_mcp_tool_to_openai_tool(mcp_tool=tool) for tool in tools]
return tools

View file

@ -1115,7 +1115,7 @@ Model Info:
message=message,
level=level,
alert_type=AlertType.model_deprecation_warnings,
alerting_metadata={ # mutable-ok: send_alert takes a dict payload
alerting_metadata={
"deprecated_count": len(snapshot.deprecated),
"imminent_count": len(snapshot.imminent),
"upcoming_count": len(snapshot.upcoming),
@ -1229,8 +1229,8 @@ Model Info:
try:
existing_invitations: Final = TypeAdapter(list[InvitationModel]).validate_python(
await InvitationLinkRepository(prisma_client).table.find_many( # pyright: ignore[reportAny] # untyped prisma boundary (any-ok), result validated by TypeAdapter
where={"user_id": recipient_user_id}, # mutable-ok: prisma find_many requires a dict where filter
order={"created_at": "desc"}, # mutable-ok: prisma find_many requires a dict order arg
where={"user_id": recipient_user_id},
order={"created_at": "desc"},
),
from_attributes=True,
)
@ -1996,7 +1996,7 @@ Model Info:
message="\n\n".join(event.message for event in typed_events),
level="High",
alert_type=alert_type,
alerting_metadata={}, # mutable-ok: send_alert takes a dict payload
alerting_metadata={},
)
for event in typed_events:
await self.internal_usage_cache.async_set_cache(

View file

@ -337,7 +337,7 @@ class AzureSentinelLogger(CustomBatchLogger):
Raises a NON Blocking verbose_logger.exception if an error occurs
"""
batch_to_send: Final = tuple(self.log_queue)
self.log_queue = [] # mutable-ok: queue ownership is detached before the async send
self.log_queue = []
try:
undelivered: Final = await self._async_send_batch_to_api(
log_queue=batch_to_send,
@ -360,7 +360,7 @@ class AzureSentinelLogger(CustomBatchLogger):
Sends the batch of audit logs to Azure Monitor Logs Ingestion API
"""
batch_to_send: Final = tuple(self.audit_log_queue)
self.audit_log_queue = [] # mutable-ok: queue ownership is detached before the async send
self.audit_log_queue = []
try:
undelivered: Final = await self._async_send_batch_to_api(
log_queue=batch_to_send,
@ -384,7 +384,7 @@ class AzureSentinelLogger(CustomBatchLogger):
queue: list[_QueuedPayload],
log_type: str,
) -> list[_QueuedPayload]:
merged: Final = [*undelivered, *queue] # mutable-ok: queue trimming returns a mutable logger queue
merged: Final = [*undelivered, *queue]
overflow: Final = len(merged) - self.max_queue_size
if overflow <= 0:
return merged

View file

@ -310,7 +310,7 @@ class CustomGuardrail(CustomLogger):
def inject_advisory_message(
self,
data: dict[str, Any], # mutable-ok: caller's dict is mutated in place, matching mark_pre_call_hook_ran
data: dict[str, Any],
message: str,
) -> bool:
"""
@ -340,7 +340,7 @@ class CustomGuardrail(CustomLogger):
land and degrade to blocking instead of silently letting the
flagged request through unmodified.
"""
advisory_message: Final = {"role": "system", "content": message} # mutable-ok: plain dict for live request
advisory_message: Final = {"role": "system", "content": message}
existing_messages: Final = data.get("messages")
existing_input: Final = data.get("input")
existing_instructions: Final = data.get("instructions")
@ -351,7 +351,7 @@ class CustomGuardrail(CustomLogger):
# model to disregard a trailing warning. Prefer it over "input"
# whenever present.
if isinstance(existing_messages, list):
messages_with_instructions_note: Final = [ # mutable-ok: fresh list
messages_with_instructions_note: Final = [
*existing_messages,
advisory_message,
]
@ -363,7 +363,7 @@ class CustomGuardrail(CustomLogger):
# real, read field (e.g. a chat-completions call carrying a stray
# "input"), so write to both when both are present.
if isinstance(existing_messages, list):
messages_with_input_note: Final = [*existing_messages, advisory_message] # mutable-ok: fresh list
messages_with_input_note: Final = [*existing_messages, advisory_message]
data["messages"] = messages_with_input_note # rebind-ok: mutates caller's dict by design
# The Responses API reads "input", not "messages" -- appending only to
# "messages" would leave the advisory unreachable for that endpoint.
@ -377,10 +377,10 @@ class CustomGuardrail(CustomLogger):
# non-delivery so the caller degrades to blocking.
return False
if isinstance(existing_messages, list):
messages_without_input_note: Final = [*existing_messages, advisory_message] # mutable-ok: fresh list
messages_without_input_note: Final = [*existing_messages, advisory_message]
data["messages"] = messages_without_input_note # rebind-ok: mutates caller's dict by design
return True
sole_message: Final = [advisory_message] # mutable-ok: plain list for the live JSON request
sole_message: Final = [advisory_message]
data["messages"] = sole_message # rebind-ok: mutates caller's dict by design
return True
@ -877,10 +877,10 @@ class CustomGuardrail(CustomLogger):
async def async_logging_hook(
self,
kwargs: dict, # mutable-ok: CustomLogger.async_logging_hook contract
kwargs: dict,
result: object,
call_type: str,
) -> tuple[dict, object]: # mutable-ok: CustomLogger.async_logging_hook contract
) -> tuple[dict, object]:
"""logging_only: run apply_guardrail on copies of the logged request/response and record the verdict."""
from litellm.llms import get_guardrail_translation_mapping
@ -918,10 +918,10 @@ class CustomGuardrail(CustomLogger):
async def _scan_logged_call(
self,
kwargs: dict, # mutable-ok: CustomLogger.async_logging_hook contract
kwargs: dict,
result: object,
translation: "BaseTranslation",
scratch_metadata: dict, # mutable-ok: apply_guardrail records its verdict into request metadata
scratch_metadata: dict,
) -> None:
optional_params: Final = kwargs.get("optional_params") or {}
scratch_input: Final = copy.deepcopy(kwargs.get("messages") or kwargs.get("input"))
@ -1482,7 +1482,7 @@ def _original_inputs_for(
kwargs: Mapping[str, object],
request_data: Mapping[str, object],
event_type: GuardrailEventHooks | None,
) -> dict | None: # mutable-ok: matches _process_response(original_inputs=) signature
) -> dict | None:
"""Baseline the hook's return value is compared against to decide "allow" vs "mask".
Hooks may edit their argument in place and return it, so the baseline is always a deep

View file

@ -397,7 +397,7 @@ class DataDogLogger(
verbose_logger.debug("[DATADOG MOCK] Batch of %s events successfully mocked", len(batch_to_send))
except BatchSendCancelled as cancelled:
self.log_queue = list(cancelled.undelivered) + self.log_queue # mutable-ok: logger queue remains appendable
self.log_queue = list(cancelled.undelivered) + self.log_queue
raise asyncio.CancelledError() from cancelled
except Exception as e:
self.log_queue = batch_to_send + self.log_queue
@ -425,7 +425,7 @@ class DataDogLogger(
drop_error_message=DD_ERRORS.DATADOG_413_ERROR.value,
non_success_handler=requeue_after_http_error,
)
return list(undelivered) # mutable-ok: caller prepends records to the logger queue
return list(undelivered)
@staticmethod
def _exceeds_intake_limits(chunk: Sequence[DatadogPayload]) -> bool:

View file

@ -131,7 +131,7 @@ def _guardrail_entry_without_prompt_carriers(entry: Mapping[str, object]) -> Map
Built as an allow-list rather than a deny-list: a key neither set classifies is dropped, so a
guardrail that records its own extra detail cannot put the caller's prompt on a redacted span.
"""
return { # mutable-ok: a fresh record built per entry, handed straight to the span serializer
return {
field: REDACTED_BY_LITELM_STRING if field in PROMPT_CARRYING_GUARDRAIL_FIELDS else value
for field, value in entry.items()
if field in _CLASSIFIED_GUARDRAIL_FIELDS

View file

@ -109,7 +109,7 @@ def _metric_record_from_payload(standard_logging_object: StandardLoggingPayload)
def _bucket_metrics(bucket_records: tuple[NewRelicMetricRecord, ...]) -> tuple[NewRelicMetric, ...]:
first: Final = bucket_records[0]
attributes: Final[Mapping[str, str]] = { # mutable-ok: JSON leaf; safe_dumps stringifies MappingProxyType
attributes: Final[Mapping[str, str]] = {
key: value[:NEWRELIC_METRIC_ATTRIBUTE_MAX_LEN]
for key, value in (
("team_id", first.team_id),
@ -150,7 +150,7 @@ def _team_budget_gauges(record: NewRelicMetricRecord) -> tuple[NewRelicMetric, .
team_max_budget: Final = record.team_max_budget
if team_max_budget is None:
return ()
attributes: Final[Mapping[str, str]] = { # mutable-ok: JSON leaf; safe_dumps stringifies MappingProxyType
attributes: Final[Mapping[str, str]] = {
key: value[:NEWRELIC_METRIC_ATTRIBUTE_MAX_LEN]
for key, value in (("team_id", record.team_id), ("team_alias", record.team_alias))
if value
@ -265,7 +265,7 @@ class NewRelicMetricsLogger(CustomBatchLogger):
dropped,
NEWRELIC_METRICS_MAX_DRAIN_PASSES,
)
self.log_queue[:] = list(survivors) # mutable-ok: leave late arrivals for the next serialized drain
self.log_queue[:] = list(survivors)
async def _drain_flush_once(self) -> None:
"""Attempt every queued record once, in ``batch_size`` chunks, without

View file

@ -79,7 +79,7 @@ def stream_output(chunks: Sequence[object], data: Mapping[str, object]) -> str |
def _assembled_chat_stream(chunks: Sequence[object], data: Mapping[str, object]) -> object:
try:
return litellm.stream_chunk_builder( # pyright: ignore[reportUnknownMemberType] # upstream types chunks as a bare list
chunks=list(chunks), # mutable-ok: stream_chunk_builder takes a list
chunks=list(chunks),
messages=_MESSAGES.validate_python(data.get("messages")),
)
except (litellm.APIError, ValidationError):

View file

@ -62,7 +62,7 @@ def encode_spans_json(spans: Sequence[ReadableSpan]) -> bytes:
class OTLPJsonSpanExporter(OTLPSpanExporter):
def __init__(self, endpoint: str | None, headers: dict[str, str]) -> None: # mutable-ok: SDK __init__ takes Dict
def __init__(self, endpoint: str | None, headers: dict[str, str]) -> None:
super().__init__(endpoint=endpoint, headers=headers)
self._session.headers["Content-Type"] = JSON_CONTENT_TYPE

View file

@ -516,9 +516,9 @@ class TenantFanOutSpanProcessor(SpanProcessor):
self._lock: Final = threading.Condition()
self._closed = False # guarded by ``_lock``: an unlocked read races the teardown it gates
self._build: Final = processor_factory if processor_factory is not None else _destination_processor
self._processors: OrderedDict[object, SpanProcessor] = OrderedDict() # mutable-ok: bounded LRU
self._retired: OrderedDict[int, SpanProcessor] = OrderedDict() # mutable-ok: drains as exports finish
self._exporting: dict[int, int] = {} # mutable-ok: per-processor in-flight export count
self._processors: OrderedDict[object, SpanProcessor] = OrderedDict()
self._retired: OrderedDict[int, SpanProcessor] = OrderedDict()
self._exporting: dict[int, int] = {}
self._drain: Final = drain_pool if drain_pool is not None else _DrainPool(capacity=pending_drains)
def on_start(self, span: SDKSpan, parent_context: Context | None = None) -> None:
@ -576,9 +576,7 @@ class TenantFanOutSpanProcessor(SpanProcessor):
live: Final = tuple((id(p), p) for p in (*self._processors.values(), *self._retired.values()))
closing: Final = tuple(p for ident, p in live if ident not in self._exporting)
self._processors.clear()
self._retired = OrderedDict( # mutable-ok: the same bounded map, keeping only what is still exporting
(ident, p) for ident, p in live if ident in self._exporting
)
self._retired = OrderedDict((ident, p) for ident, p in live if ident in self._exporting)
for processor in closing:
self._drain.submit(processor)
self._drain.close(timeout=max(0.0, deadline - time.monotonic()))

View file

@ -171,12 +171,10 @@ class TenantTracerCache:
# thread-pool workers concurrently with the event loop, so cache
# updates, span counts, and retirement must be atomic.
self._lock: Final = threading.Lock()
self._providers: OrderedDict[_RouteKey, TracerProvider] = (
OrderedDict() # mutable-ok: bounded LRU; eviction needs in-place ordered mutation
)
self._open_span_counts: dict[TracerProvider, int] = {} # mutable-ok: live refcount state
self._providers: OrderedDict[_RouteKey, TracerProvider] = OrderedDict()
self._open_span_counts: dict[TracerProvider, int] = {}
# Oldest-first so an overflow of draining providers sheds the stalest.
self._retired: OrderedDict[TracerProvider, None] = OrderedDict() # mutable-ok: draining evicted providers
self._retired: OrderedDict[TracerProvider, None] = OrderedDict()
# An owned exporter is routable only when its kind actually resolves to a
# header-carrying OTLP exporter. A denylist would accept a typo'd or
# unavailable kind, which ``_exporter_from_spec`` falls back to a
@ -395,7 +393,7 @@ class TenantTracerCache:
if project_headers and kind not in _GRPC_KINDS
else base
)
update: Final = { # mutable-ok: model_copy(update=...) requires a plain dict
update: Final = {
field: value
for field, value in (("headers", routed), ("endpoint", endpoint))
if (field == "headers" and routed != spec.headers)

View file

@ -145,7 +145,7 @@ def destination_for(
endpoint, protocol = resolved
return OtelDestination(
endpoint=endpoint,
headers=MappingProxyType(dict(headers)), # mutable-ok: MappingProxyType needs a concrete mapping to wrap
headers=MappingProxyType(dict(headers)),
resource_attributes=MappingProxyType({"service.name": service_name}) if service_name else _NO_ATTRS,
callback_name=callback_name,
protocol=protocol,

View file

@ -30,7 +30,7 @@ def langfuse_preset(
if not allow_missing_credentials:
raise
return base.model_copy(
update={ # mutable-ok: pydantic model_copy takes a plain update mapping
update={
"exporters": credential_gated_exporters(base.exporters, ExporterOwner.LANGFUSE_OTEL),
"mapper_names": mappers,
}

View file

@ -31,7 +31,7 @@ def weave_preset(
if not allow_missing_credentials:
raise
return base.model_copy(
update={ # mutable-ok: pydantic model_copy takes a plain update mapping
update={
"exporters": credential_gated_exporters(base.exporters, ExporterOwner.WEAVE_OTEL),
"mapper_names": mappers,
}

View file

@ -207,9 +207,7 @@ class PointFiveLogger(CustomBatchLogger):
the excluded-field list and this callback's own setting are applied here, then the
global, per-request and header settings that only the framework's predicate knows.
"""
details: Final = self.redact_standard_logging_payload_from_model_call_details(
dict(kwargs) # mutable-ok: both framework helpers take the call details as a dict
)
details: Final = self.redact_standard_logging_payload_from_model_call_details(dict(kwargs))
payload: Final = details.get("standard_logging_object")
if not isinstance(payload, dict):
return None

View file

@ -147,7 +147,7 @@ class PointFiveUploadClient:
response: Final = await self.http_client.post(
self.api_url + path,
json=request.model_dump(by_alias=True),
headers={ # mutable-ok: AsyncHTTPHandler.post types headers as dict
headers={
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
},
@ -172,7 +172,7 @@ class PointFiveUploadClient:
if isinstance(destination, PointFiveUploadFailure):
return destination
url, host = destination
headers: Final = dict(PUT_HEADERS, Host=host) if host else dict(PUT_HEADERS) # mutable-ok: put wants dict
headers: Final = dict(PUT_HEADERS, Host=host) if host else dict(PUT_HEADERS)
try:
await self.http_client.put(url, data=body, headers=headers, follow_redirects=False)
except httpx.HTTPStatusError as e:

View file

@ -1232,7 +1232,7 @@ class PrometheusLogger(CustomLogger):
return metric_class(*args, **kwargs)
kept: Final = tuple(name for name in original_labelnames if name not in self.exclude_labels)
kept_kwargs: Final = {**kwargs, "labelnames": kept} # mutable-ok: ** needs a mapping to override labelnames
kept_kwargs: Final = {**kwargs, "labelnames": kept}
real_metric: Final = metric_class(*args, **kept_kwargs)
return _ExcludedLabelMetric(real_metric, original_labelnames, self.exclude_labels)

View file

@ -239,7 +239,7 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM):
def _sign_put(
self, credentials: "Credentials", url: str, json_string: str, headers: Mapping[str, str]
) -> dict[str, str]: # mutable-ok: [LIT001] AsyncHTTPHandler.put/HTTPHandler.put only accept dict headers
) -> dict[str, str]:
"""
``RefreshableCredentials`` (IMDS roles) may refresh between the access key, secret and token
reads SigV4 performs, producing a mixed-generation signature that S3 rejects with 403.

View file

@ -748,7 +748,7 @@ def _as_active_job(record: object, attempts: int, spend: float) -> ActiveShadowE
except ValidationError as e:
verbose_logger.debug("shadow_eval: skipping unsamplable job row: %s", e)
return None
return job.model_copy(update={"attempts": attempts, "spend": spend}) # mutable-ok: pydantic update payload
return job.model_copy(update={"attempts": attempts, "spend": spend})
_jobs_cache: Final = InMemoryCache(max_size_in_memory=4, default_ttl=_JOBS_CACHE_TTL_SECONDS)
@ -785,7 +785,7 @@ class ShadowEvalLogger(CustomLogger):
self._inflight_shadow_tasks: int = 0
# Starts per job since the last cache fill, never decremented within a
# generation; the refill absorbs written rows and resets.
self._job_starts: dict[str, int] = {} # mutable-ok: per-generation counter
self._job_starts: dict[str, int] = {}
async def _active_jobs(self) -> Mapping[tuple[str, str], tuple[ActiveShadowEvalJob, ...]]:
"""Active jobs by (target_type, target_id), cache-first. A target holds at most
@ -799,23 +799,22 @@ class ShadowEvalLogger(CustomLogger):
return _EMPTY_JOBS
try:
records: Final = await prisma.db.litellm_shadowevaljob.find_many(
where={ # mutable-ok: Prisma filter
where={
"stopped_at": None,
"ends_at": {"gt": datetime.now(timezone.utc)}, # mutable-ok: Prisma filter
"ends_at": {"gt": datetime.now(timezone.utc)},
},
)
grouped: Final = (
await prisma.db.litellm_shadowevalattempt.group_by(
by=["job_id"],
count=True,
# mutable-ok: Prisma aggregate spec
sum={"judge_cost": True, "shadow_cost": True, "shadow_classifier_cost": True},
where={"job_id": {"in": [str(record.id) for record in records]}}, # mutable-ok: Prisma filter
where={"job_id": {"in": [str(record.id) for record in records]}},
)
if records
else ()
)
attempt_stats: Final = { # mutable-ok: frozen snapshot of the grouped read
attempt_stats: Final = {
str(row["job_id"]): (
int(row["_count"]["_all"]),
_leg_eval_spend(row["_sum"] or _EMPTY_METADATA),
@ -886,13 +885,13 @@ class ShadowEvalLogger(CustomLogger):
payload: Final[StandardLoggingPayload | None] = kwargs.get("standard_logging_object") # pyright: ignore[reportAssignmentType] # untyped callback kwargs
if payload is None:
return
raw_meta: Final = get_litellm_metadata_from_kwargs(dict(kwargs)) # mutable-ok: helper needs dict
raw_meta: Final = get_litellm_metadata_from_kwargs(dict(kwargs))
request_metadata: Final = raw_meta if isinstance(raw_meta, Mapping) else _EMPTY_METADATA
if request_metadata.get(INTERNAL_CALL_ORIGIN_METADATA_KEY):
return # internal sub-call (our own shadow/judge, a classifier), not user traffic
# redaction rewrites logged content before callbacks run, so this hook
# only ever sees placeholders for a redacted request
if should_redact_message_logging(dict(kwargs)): # mutable-ok: predicate takes a plain dict
if should_redact_message_logging(dict(kwargs)):
return
metadata: Final = payload.get("metadata") or _EMPTY_METADATA
# Each identity the request resolved to is a candidate target; JWT-auth
@ -928,7 +927,7 @@ class ShadowEvalLogger(CustomLogger):
sample: Final = _judgeable_sample(
ops,
kwargs,
MappingProxyType(dict(payload.get("model_parameters") or {})), # mutable-ok: frozen snapshot
MappingProxyType(dict(payload.get("model_parameters") or {})),
response_obj,
)
if sample is None:
@ -961,7 +960,7 @@ class ShadowEvalLogger(CustomLogger):
real_cache_hit=real_cache_hit,
control_tier=control_tier,
shadow_params=shadow_params,
parent_metadata=MappingProxyType(dict(request_metadata)), # mutable-ok: frozen snapshot
parent_metadata=MappingProxyType(dict(request_metadata)),
)
).add_done_callback(self._release_shadow_slot)
except Exception as e: # noqa: BLE001 # logging hooks must never fail the request
@ -1175,7 +1174,7 @@ class ShadowEvalLogger(CustomLogger):
return
try:
await prisma.db.litellm_shadowevalattempt.create(
data={ # mutable-ok: Prisma payload
data={
"job_id": job.id,
"request_id": request_id,
"router_name": router_name,
@ -1210,18 +1209,16 @@ class ShadowEvalLogger(CustomLogger):
router: Final = self._router_provider()
if router is None:
return _CallFailure("no router configured on this pod")
shadow_metadata: Final[dict[str, object]] = ( # mutable-ok: router writes its routing decision back
sanitized_forwardable_call_metadata(parent_metadata, SHADOW_EVAL_ROUTER_CALL_ORIGIN)
shadow_metadata: Final[dict[str, object]] = sanitized_forwardable_call_metadata(
parent_metadata, SHADOW_EVAL_ROUTER_CALL_ORIGIN
)
try:
response: Final = await router.acompletion(
model=target_model,
messages=[ # mutable-ok: provider transforms rewrite messages in place, so the router gets its own copy
dict(m) for m in messages
], # pyright: ignore[reportArgumentType] # snapshot of the SDK's own message dicts
messages=[dict(m) for m in messages], # pyright: ignore[reportArgumentType] # snapshot of the SDK's own message dicts
metadata=shadow_metadata,
num_retries=0,
fallbacks=[], # mutable-ok: SDK kwarg; a failed shadow is a recorded error, never a spend multiplier
fallbacks=[],
**shadow_params,
)
except Exception as e: # noqa: BLE001 # provider errors become error rows, not crashes
@ -1270,12 +1267,12 @@ class ShadowEvalLogger(CustomLogger):
if m.get("content") is not None
)
judge_metadata: Final = sanitized_forwardable_call_metadata(parent_metadata, SHADOW_EVAL_JUDGE_CALL_ORIGIN)
judge_messages: Final = [ # mutable-ok: SDK takes a list
{"role": "system", "content": PAIRWISE_JUDGE_SYSTEM_PROMPT}, # mutable-ok: SDK message
judge_messages: Final = [
{"role": "system", "content": PAIRWISE_JUDGE_SYSTEM_PROMPT},
{
"role": "user",
"content": _judge_user_prompt(conversation, response_a, response_b, _tool_definitions_text(tools)),
}, # mutable-ok: SDK message
},
]
try:
response: Final = await judge_acompletion(

View file

@ -1594,7 +1594,7 @@ class WebSearchInterceptionLogger(CustomLogger):
user_api_key_metadata: Final[StandardLoggingUserAPIKeyMetadata] = (
LiteLLMProxyRequestSetup.get_sanitized_user_information_from_key(user_api_key_dict=user_api_key_auth)
)
return { # mutable-ok: litellm's metadata channel is a plain dict its logging path reads and enriches
return {
**user_api_key_metadata,
"model_group": search_tool_name,
"user_api_key": user_api_key_auth.api_key,

View file

@ -79,9 +79,7 @@ async def _fetch_interaction(context: BackgroundInteractionPollContext) -> Inter
custom_llm_provider=context.custom_llm_provider,
api_key=context.api_key,
api_base=context.api_base,
**{
"no-log": True
}, # mutable-ok: "no-log" is not a valid identifier, so it can only be passed through a mapping
**{"no-log": True},
)
@ -240,7 +238,7 @@ class _ActiveBackgroundPoll:
context: BackgroundInteractionPollContext
_ACTIVE_POLLS: dict[str, _ActiveBackgroundPoll] = {} # mutable-ok: asyncio needs strong refs to running poll tasks
_ACTIVE_POLLS: dict[str, _ActiveBackgroundPoll] = {}
def _discard_poll(interaction_id: str, task: "asyncio.Task[None]") -> None:

View file

@ -500,8 +500,8 @@ def safe_deep_copy(data):
def independent_snapshot(
data: dict, # mutable-ok: caller-defined request-payload shape
) -> dict: # mutable-ok: caller-defined request-payload shape
data: dict,
) -> dict:
"""
A copy of ``data`` whose top-level keys are deep-copied independently
where possible -- always attempted, regardless of
@ -522,7 +522,7 @@ def independent_snapshot(
"""
sanitized: Final = {
key: (
{ # mutable-ok: same request-payload shape as data
{
inner_key: ("placeholder" if inner_key == "litellm_parent_otel_span" else inner_value)
for inner_key, inner_value in value.items()
}
@ -544,15 +544,13 @@ def independent_snapshot(
and isinstance(original_value, dict)
and "litellm_parent_otel_span" in original_value
):
return { # mutable-ok: same request-payload shape as data
return {
**copied_value,
"litellm_parent_otel_span": original_value["litellm_parent_otel_span"],
}
return copied_value
return { # mutable-ok: same request-payload shape as data
key: _copied_value(key, value) for key, value in sanitized.items()
}
return {key: _copied_value(key, value) for key, value in sanitized.items()}
def filter_exceptions_from_params(data: object, max_depth: int = 20) -> Any:

View file

@ -195,7 +195,7 @@ def mark_litellm_import_complete() -> None:
@dataclass(frozen=True, slots=True)
class ModelCostMapReloaded:
model_cost_map: dict # mutable-ok: adopted as litellm.model_cost, whose consumer contract is a plain mutable dict
model_cost_map: dict
revision: str | None = None
etag: str | None = None
@ -537,7 +537,7 @@ def _finalize_loaded_model_cost_map(loaded: ModelCostMapReloaded) -> ModelCostMa
def adopt_model_cost_map(
new_model_cost_map: dict, # mutable-ok: public API preserves the mutable cost-map contract
new_model_cost_map: dict,
) -> int:
import litellm
from litellm import utils
@ -639,7 +639,7 @@ def get_model_cost_map(
if isinstance(outcome, _FetchAttemptRetryable) and max_attempts > 1:
threading.Thread(
target=_retry_remote_fetch_in_background,
kwargs={ # mutable-ok: threading requires a mutable keyword-arguments mapping
kwargs={
"url": url,
"timeout": timeout,
"max_attempts": max_attempts,

View file

@ -112,16 +112,16 @@ def sanitize_user_api_key_auth(auth: object) -> object:
"""Copy of the auth object with its budget reservation removed; the cost callback
falls back to reading the reservation from inside the auth object."""
if isinstance(auth, dict):
return {k: v for k, v in auth.items() if k != "budget_reservation"} # mutable-ok: SDK metadata value
return {k: v for k, v in auth.items() if k != "budget_reservation"}
reservation: Final[object] = getattr(auth, "budget_reservation", None)
model_copy: Final[object] = getattr(auth, "model_copy", None)
if reservation is not None and callable(model_copy):
return model_copy(update={"budget_reservation": None}) # mutable-ok: pydantic update payload
return model_copy(update={"budget_reservation": None})
return auth
def _sanitized(parent_metadata: Mapping[str, object]) -> dict[str, object]: # mutable-ok: SDK metadata kwarg
return { # mutable-ok: SDK metadata kwarg
def _sanitized(parent_metadata: Mapping[str, object]) -> dict[str, object]:
return {
k: sanitize_user_api_key_auth(v) if k == _USER_API_KEY_AUTH_KEY else v
for k, v in parent_metadata.items()
if k not in BUDGET_RESERVATION_METADATA_KEYS
@ -131,17 +131,15 @@ def _sanitized(parent_metadata: Mapping[str, object]) -> dict[str, object]: # m
def forwarded_internal_call_metadata(
parent_metadata: Mapping[str, object] | None,
call_origin: InternalCallOrigin,
) -> dict[str, object]: # mutable-ok: SDK metadata kwarg
) -> dict[str, object]:
"""Parent metadata, minus its budget reservation, stamped with the sub-call's origin.
For sub-calls made inside the parent request (classifier, embeddings), where the
parent's full context still describes the call being made.
"""
if not parent_metadata:
return {} # mutable-ok: SDK metadata kwarg
return _sanitized(parent_metadata) | { # mutable-ok: SDK metadata kwarg
INTERNAL_CALL_ORIGIN_METADATA_KEY: call_origin
}
return {}
return _sanitized(parent_metadata) | {INTERNAL_CALL_ORIGIN_METADATA_KEY: call_origin}
def parent_session_kwargs(request_kwargs: Mapping[str, object] | None) -> Mapping[str, str]:
@ -160,11 +158,11 @@ def effective_turn_off_message_logging(request_kwargs: Mapping[str, object] | No
def sanitized_forwardable_call_metadata(
parent_metadata: Mapping[str, object],
call_origin: InternalCallOrigin,
) -> dict[str, object]: # mutable-ok: SDK metadata kwarg
) -> dict[str, object]:
"""Just the caller's identity, stamped with the sub-call's origin.
For sub-calls detached from the parent request (shadow eval), which outlive it and
must not inherit per-request state such as its routing decision or logging payload.
"""
identity: Final = {k: v for k, v in parent_metadata.items() if k in FORWARDABLE_IDENTITY_METADATA_KEYS}
return _sanitized(identity) | {INTERNAL_CALL_ORIGIN_METADATA_KEY: call_origin} # mutable-ok: SDK metadata kwarg
return _sanitized(identity) | {INTERNAL_CALL_ORIGIN_METADATA_KEY: call_origin}

View file

@ -21,20 +21,18 @@ class JSONFragmentAccumulator:
def __init__(self) -> None:
self._chunks: list[str] = [] # mutable-ok: O(1) append; string concat would copy the buffer each time
self._buffer: str = (
"" # mutable-ok: lazily materialized join of _chunks, rebuilt only when _chunks is non-empty
)
self._offset: int = 0 # mutable-ok: cursor past already-consumed values; avoids re-slicing on every pop
self._could_close: bool = False # mutable-ok: cached heuristic; rescanning past fragments was itself O(n^2)
self._buffer: str = ""
self._offset: int = 0
self._could_close: bool = False
def __bool__(self) -> bool:
return bool(self._chunks) or self._offset < len(self._buffer)
def append(self, fragment: str) -> None:
self._chunks.append(fragment) # mutable-ok: see __init__
self._chunks.append(fragment)
stripped: Final = fragment.rstrip()
if stripped:
self._could_close = stripped[-1] in ("}", "]") # mutable-ok: see __init__
self._could_close = stripped[-1] in ("}", "]")
def could_close_json(self) -> bool:
"""
@ -50,9 +48,9 @@ class JSONFragmentAccumulator:
if not self._chunks:
return
unconsumed: Final = self._buffer[self._offset :]
self._buffer = unconsumed + "".join(self._chunks) # mutable-ok: merge pending fragments, once per append batch
self._offset = 0 # mutable-ok: see __init__
self._chunks = [] # mutable-ok: see __init__
self._buffer = unconsumed + "".join(self._chunks)
self._offset = 0
self._chunks = []
def pop_next_value(self) -> tuple[bool, object]:
"""
@ -69,7 +67,7 @@ class JSONFragmentAccumulator:
while start < length and self._buffer[start].isspace():
start += 1
if start >= length:
self._offset = start # mutable-ok: see __init__
self._offset = start
return False, None
decoder: Final = json.JSONDecoder()
try:
@ -77,11 +75,11 @@ class JSONFragmentAccumulator:
except json.JSONDecodeError:
return False, None
decoded, end_index = cast("tuple[object, int]", raw_value) # cast-ok: raw_decode returns tuple[Any, int]
self._offset = end_index # mutable-ok: see __init__
self._offset = end_index
if self._offset >= len(self._buffer):
self._buffer = "" # mutable-ok: see __init__
self._offset = 0 # mutable-ok: see __init__
self._could_close = False # mutable-ok: buffer is empty, nothing can close
self._buffer = ""
self._offset = 0
self._could_close = False
return True, decoded
def snapshot(self) -> str:
@ -90,8 +88,8 @@ class JSONFragmentAccumulator:
def set(self, value: str) -> None:
"""Replace the buffer's contents with a single fragment."""
self._chunks = [] # mutable-ok: see __init__
self._buffer = value # mutable-ok: see __init__
self._offset = 0 # mutable-ok: see __init__
self._chunks = []
self._buffer = value
self._offset = 0
stripped: Final = value.rstrip()
self._could_close = bool(stripped) and stripped[-1] in ("}", "]") # mutable-ok: see __init__
self._could_close = bool(stripped) and stripped[-1] in ("}", "]")

View file

@ -619,7 +619,7 @@ class Logging(LiteLLMLoggingBaseClass):
self.caching_details: CachingDetails | None = None
# Timing for results that cannot carry ``_hidden_params`` (plain-dict /v1/messages
# responses and the bridge stream wrappers); see ``update_response_metadata``.
self.response_timing_metrics: Mapping[str, float] = {} # mutable-ok: kept deep-copyable
self.response_timing_metrics: Mapping[str, float] = {}
# Passthrough endpoint guardrails config for field targeting
self.passthrough_guardrails_config: dict[str, Any] | None = None
@ -642,7 +642,7 @@ class Logging(LiteLLMLoggingBaseClass):
def set_response_timing_metrics(self, timing_metrics: Mapping[str, float]) -> None:
"""Keep ``_response_ms`` / ``litellm_overhead_time_ms`` for a result that has no ``_hidden_params``."""
self.response_timing_metrics = dict(timing_metrics) # mutable-ok: kept deep-copyable
self.response_timing_metrics = dict(timing_metrics)
def process_dynamic_callbacks(self):
"""
@ -969,7 +969,7 @@ class Logging(LiteLLMLoggingBaseClass):
prompt_management_logger: CustomLogger | None = None,
prompt_label: str | None = None,
prompt_version: int | None = None,
request_kwargs: dict[str, object] | None = None, # mutable-ok: marker stamped into live request kwargs
request_kwargs: dict[str, object] | None = None,
injected_for_every_deployment: bool = False,
) -> tuple[str, list[AllMessageValues], dict]:
from litellm.integrations.anthropic_cache_control_hook import AnthropicCacheControlHook
@ -1020,7 +1020,7 @@ class Logging(LiteLLMLoggingBaseClass):
tools: list[dict] | None = None,
prompt_label: str | None = None,
prompt_version: int | None = None,
request_kwargs: dict[str, object] | None = None, # mutable-ok: marker stamped into live request kwargs
request_kwargs: dict[str, object] | None = None,
injected_for_every_deployment: bool = False,
) -> tuple[str, list[AllMessageValues], dict]:
from litellm.integrations.anthropic_cache_control_hook import AnthropicCacheControlHook
@ -3881,7 +3881,7 @@ class Logging(LiteLLMLoggingBaseClass):
if result.status == "completed":
return InteractionsAPIResponse(
**result.model_dump(
exclude={ # mutable-ok: pydantic types exclude as set[str], which a frozenset does not satisfy
exclude={
"event_type",
"delta",
"index",
@ -4944,9 +4944,7 @@ def _has_operator_exporter(config: "OpenTelemetryV2Config") -> bool:
def _only_the_gated_exporter(config: "OpenTelemetryV2Config") -> "OpenTelemetryV2Config":
return config.model_copy(
update={"exporters": [spec for spec in config.exporters if _is_gated(spec)]} # mutable-ok: model_copy update
)
return config.model_copy(update={"exporters": [spec for spec in config.exporters if _is_gated(spec)]})
def _is_gated(spec: "ExporterSpec") -> bool:
@ -5398,7 +5396,7 @@ class StandardLoggingPayloadSetup:
if key not in user_metadata
}
)
return {**user_metadata, **model_metadata} # mutable-ok: function contract returns a plain dict
return {**user_metadata, **model_metadata}
@staticmethod
def get_standard_logging_metadata(
@ -6237,9 +6235,7 @@ def get_standard_logging_object_payload(
if clean_hidden_params["litellm_overhead_time_ms"] is None and status == "success":
# /v1/messages dict results and the bridge stream wrappers keep it on the logging object;
# failure payloads stay None like every response type that carries its own _hidden_params
timing_metrics: Final = (
getattr(logging_obj, "response_timing_metrics", None) or {} # mutable-ok: empty fallback
)
timing_metrics: Final = getattr(logging_obj, "response_timing_metrics", None) or {}
clean_hidden_params["litellm_overhead_time_ms"] = timing_metrics.get("litellm_overhead_time_ms")
model_cost_information: Final = StandardLoggingPayloadSetup.get_model_cost_information(

View file

@ -79,7 +79,7 @@ def bedrock_guardrail_cost_by_unit(
pricing: Final = _bedrock_guardrail_pricing(aws_region_name)
if pricing is None:
return None
return { # mutable-ok: stamped into guardrail_information, which safe_dumps only serializes as a plain dict
return {
counter: _priced_units(units, pricing.guardrail_cost_per_unit.get(counter))
for counter, units in usage_units.items()
}

View file

@ -27,7 +27,7 @@ def default_router_provider() -> Router | None:
return llm_router
def parse_json_verdict(raw: str) -> dict[str, object]: # mutable-ok: plain parsed-JSON payload
def parse_json_verdict(raw: str) -> dict[str, object]:
"""Parse a judge's JSON verdict, tolerating markdown fences and surrounding prose."""
text = raw.strip() # rebind-ok: progressively narrowed to the JSON payload
fenced: Final = JSON_FENCE_RE.search(text)
@ -44,7 +44,7 @@ def parse_json_verdict(raw: str) -> dict[str, object]: # mutable-ok: plain pars
parsed = json.loads(text[start : end + 1])
if not isinstance(parsed, dict):
raise ValueError("judge response is not a JSON object")
return {str(k): v for k, v in parsed.items()} # mutable-ok: plain parsed-JSON payload
return {str(k): v for k, v in parsed.items()}
def extract_text_from_content(content: object) -> str:

View file

@ -16,9 +16,7 @@ def _form_field_value(value: object) -> str:
def _flatten_form_field(key: str, value: object) -> tuple[tuple[str, str], ...]:
pending_fields: Final[ # mutable-ok: depth-capped stack walks nested JSON into multipart names
list[tuple[str, object, int]]
] = [ # mutable-ok: depth-capped stack walks nested JSON into multipart names
(key, value, 0)
]
] = [(key, value, 0)]
flat_fields: Final[list[tuple[str, str]]] = [] # mutable-ok: local accumulator
while pending_fields:
current_key, current_value, depth = pending_fields.pop()
@ -48,9 +46,7 @@ def _is_form_scalar(value: object) -> bool:
def _flatten_form_data_field(key: str, value: object) -> tuple[tuple[str, str | tuple[str, ...]], ...]:
pending_fields: Final[ # mutable-ok: depth-capped stack walks nested JSON into multipart names
list[tuple[str, object, int]]
] = [ # mutable-ok: depth-capped stack walks nested JSON into multipart names
(key, value, 0)
]
] = [(key, value, 0)]
flat_fields: Final[list[tuple[str, str | tuple[str, ...]]]] = [] # mutable-ok: local accumulator
while pending_fields:
current_key, current_value, depth = pending_fields.pop()

View file

@ -372,9 +372,7 @@ from collections import defaultdict
def _handle_invalid_parallel_tool_calls(
tool_calls: list[
ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall
], # mutable-ok: patched in place via slice assignment
tool_calls: list[ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall],
):
"""
Handle hallucinated parallel tool call from openai - https://community.openai.com/t/model-tries-to-call-unknown-function-multi-tool-use-parallel/490653

View file

@ -30,7 +30,7 @@ def response_timing_metrics(
"""
total_response_time_ms: Final = (end_time - start_time).total_seconds() * 1000
if not include_overhead:
return {"_response_ms": total_response_time_ms} # mutable-ok: read-only timing result
return {"_response_ms": total_response_time_ms}
caching_details: Final = logging_obj.caching_details
cache_duration_ms: Final = (
caching_details.get("cache_duration_ms")

View file

@ -308,7 +308,7 @@ def _set_duration_in_model_call_details(
def speech_request_body(model: str, voice: str, optional_params: Mapping[str, object]) -> Mapping[str, object]:
"""Speech request body for telemetry, without the caller headers the provider SDKs
take as request kwargs rather than body fields."""
return { # mutable-ok: loggers isinstance-check the request body as a dict
return {
"model": model,
"voice": voice,
**{key: value for key, value in optional_params.items() if key != "extra_headers"},

View file

@ -61,7 +61,7 @@ class LoggingWorker:
self._queue: asyncio.Queue[LoggingTask] | None = None
self._worker_task: asyncio.Task | None = None
self._running_tasks: set[asyncio.Task] = set()
self._dequeued_tasks: dict[int, LoggingTask] = {} # mutable-ok: refs so flush can rescue never-started tasks
self._dequeued_tasks: dict[int, LoggingTask] = {}
self._sem: asyncio.Semaphore | None = None
self._bound_loop: asyncio.AbstractEventLoop | None = None
self._last_aggressive_clear_time: float = 0.0

View file

@ -1217,7 +1217,7 @@ def _mergeable_branch(
branch: object,
seen_refs: frozenset[str],
depth: int,
expanded_refs: dict[str, Mapping[str, object] | None], # mutable-ok: per-call memo bounding repeated $ref work
expanded_refs: dict[str, Mapping[str, object] | None],
) -> Mapping[str, object] | None:
if not isinstance(branch, dict) or depth > _MAX_SCHEMA_FLATTEN_DEPTH:
return None
@ -1250,7 +1250,7 @@ def _flatten_schema_against_root(
root: Mapping[str, object],
seen_refs: frozenset[str],
depth: int,
expanded_refs: dict[str, Mapping[str, object] | None], # mutable-ok: per-call memo bounding repeated $ref work
expanded_refs: dict[str, Mapping[str, object] | None],
) -> Mapping[str, object]:
raw_branch_groups: Final = tuple(
(
@ -1282,7 +1282,7 @@ def _flatten_schema_against_root(
if not is_object_schema:
return schema
merged_properties: Final = { # mutable-ok: tool parameters are JSON dicts
merged_properties: Final = {
name: value for source in (*reversed(branches), schema) for name, value in _schema_properties(source).items()
}
required_names: Final = _schema_required_names(schema).union(
@ -1290,7 +1290,7 @@ def _flatten_schema_against_root(
)
kept: Final = MappingProxyType({key: value for key, value in schema.items() if key not in dropped})
required_update: Final = MappingProxyType({"required": sorted(required_names)}) if required_names else _EMPTY_SCHEMA
return { # mutable-ok: tool parameters are JSON dicts
return {
**kept,
"type": "object",
"properties": merged_properties,
@ -1317,7 +1317,7 @@ def flatten_top_level_schema_combinators(schema: Mapping[str, object]) -> Mappin
OpenAI's own validation still applies. Non-object schemas pass through
unchanged and the input is never mutated.
"""
return _flatten_schema_against_root(schema, schema, frozenset(), 0, {}) # mutable-ok: fresh per-call $ref memo
return _flatten_schema_against_root(schema, schema, frozenset(), 0, {})
_SUBSCHEMA_KEYWORDS: Final = frozenset(
@ -1362,7 +1362,7 @@ def drop_non_python_regex_patterns(schema: Mapping[str, object]) -> Mapping[str,
at more schema levels than a JSON parser admits, so a cyclic schema built in
code cannot spin it.
"""
rebuilt: dict[int, Mapping[str, object]] = {} # mutable-ok: per-call memo of rewritten nodes, deepest level first
rebuilt: dict[int, Mapping[str, object]] = {}
for level in reversed(tuple(islice(_schema_levels(schema), _MAX_SCHEMA_NESTING))):
rebuilt.update(
(id(node), rewritten)
@ -1392,7 +1392,7 @@ def _subschemas(node: Mapping[str, object]) -> Iterator[Mapping[str, object]]:
def _node_without_non_python_regex(
node: Mapping[str, object], rebuilt: Mapping[int, Mapping[str, object]]
) -> Mapping[str, object]:
kept: Final = { # mutable-ok: tool parameters are JSON dicts
kept: Final = {
key: _keyword_value_rebuilt(key, value, rebuilt)
for key, value in node.items()
if key != "pattern" or not isinstance(value, str) or _is_python_regex(value)
@ -1402,14 +1402,14 @@ def _node_without_non_python_regex(
def _keyword_value_rebuilt(key: str, value: object, rebuilt: Mapping[int, Mapping[str, object]]) -> object:
if key in _SUBSCHEMA_MAP_KEYWORDS and isinstance(value, dict):
kept: Final = { # mutable-ok: tool parameters are JSON dicts
kept: Final = {
name: rebuilt.get(id(sub), sub)
for name, sub in value.items()
if key != "patternProperties" or not isinstance(name, str) or _is_python_regex(name)
}
return value if len(kept) == len(value) and all(kept[name] is value[name] for name in kept) else kept
if key in _SUBSCHEMA_LIST_KEYWORDS and isinstance(value, list):
items: Final = [rebuilt.get(id(sub), sub) for sub in value] # mutable-ok: tool parameters are JSON lists
items: Final = [rebuilt.get(id(sub), sub) for sub in value]
return value if all(new is old for new, old in zip(items, value, strict=True)) else items
if key in _SUBSCHEMA_KEYWORDS and isinstance(value, dict):
return rebuilt.get(id(value), value)
@ -1441,7 +1441,7 @@ def tool_with_sanitized_parameters(
sanitized: Final = sanitize(parameters)
if sanitized is parameters:
return tool
return {**tool, "function": {**function, "parameters": sanitized}} # mutable-ok: request tools are JSON dicts
return {**tool, "function": {**function, "parameters": sanitized}}
def _get_image_mime_type_from_url(url: str) -> str | None:
@ -1700,7 +1700,7 @@ _MarkedT: Final = TypeVar("_MarkedT", bound=Mapping[str, object])
def with_prompt_cache_breakpoint(target: _MarkedT, marker: object) -> _MarkedT:
if marker is None:
return target
marked: Final = {**target, "prompt_cache_breakpoint": marker} # mutable-ok: API message payload
marked: Final = {**target, "prompt_cache_breakpoint": marker}
return cast(_MarkedT, marked) # cast-ok: same block shape as the input plus the marker key
@ -1714,9 +1714,7 @@ def strip_litellm_internal_message_fields(message: AllMessageValues) -> AllMessa
return message
return cast( # cast-ok: same TypedDict minus internal keys
AllMessageValues,
{ # mutable-ok: provider transforms mutate message dicts in place downstream
key: value for key, value in message.items() if key not in LITELLM_INTERNAL_MESSAGE_FIELDS
},
{key: value for key, value in message.items() if key not in LITELLM_INTERNAL_MESSAGE_FIELDS},
)
@ -2166,11 +2164,9 @@ def _split_images_from_tool_message(
)
if not image_parts:
return message, ()
remaining_parts = [ # mutable-ok: tool message content must stay a json list
part for part in content if not _is_image_url_part(part)
]
remaining_parts = [part for part in content if not _is_image_url_part(part)]
new_content = remaining_parts if remaining_parts else TOOL_RESULT_IMAGE_PLACEHOLDER
rewritten = {**message, "content": new_content} # mutable-ok: chat messages are plain json dicts
rewritten = {**message, "content": new_content}
return cast(AllMessageValues, rewritten), image_parts # cast-ok: dict spread keeps keys like cache_control
@ -2178,14 +2174,12 @@ def _hoist_images_in_tool_message_run(
run: Iterable[AllMessageValues],
) -> list[AllMessageValues]: # mutable-ok: message pipelines type messages as mutable lists
split_results = tuple(_split_images_from_tool_message(message) for message in run)
hoisted_images = [ # mutable-ok: user message content must be a json list
image for _, images in split_results for image in images
]
rewritten_messages = [message for message, _ in split_results] # mutable-ok: pipelines mutate message lists
hoisted_images = [image for _, images in split_results for image in images]
rewritten_messages = [message for message, _ in split_results]
if not hoisted_images:
return rewritten_messages
boundary_part = ChatCompletionTextObject(type="text", text=TOOL_RESULT_IMAGE_BOUNDARY)
hoisted_content = [boundary_part, *hoisted_images] # mutable-ok: user message content must be a json list
hoisted_content = [boundary_part, *hoisted_images]
rewritten_messages.append(ChatCompletionUserMessage(role="user", content=hoisted_content))
return rewritten_messages
@ -2209,7 +2203,7 @@ def hoist_images_from_tool_messages(
"""
if not any(_tool_message_carries_image(message) for message in messages):
return messages
return [ # mutable-ok: pipelines mutate message lists
return [
rewritten_message
for is_tool_run, run in groupby(messages, key=lambda message: message.get("role") == "tool")
for rewritten_message in (_hoist_images_in_tool_message_run(run) if is_tool_run else run)
@ -2231,11 +2225,9 @@ def _drop_tool_reference_parts(message: AllMessageValues) -> AllMessageValues:
if not _tool_message_carries_tool_reference(message):
return message
content = cast(list, message.get("content")) # cast-ok: shape checked by _tool_message_carries_tool_reference
remaining_parts = [ # mutable-ok: tool message content must stay a json list
part for part in content if not _is_tool_reference_part(part)
]
remaining_parts = [part for part in content if not _is_tool_reference_part(part)]
new_content = remaining_parts if remaining_parts else ""
rewritten = {**message, "content": new_content} # mutable-ok: chat messages are plain json dicts
rewritten = {**message, "content": new_content}
return cast(AllMessageValues, rewritten) # cast-ok: dict spread keeps keys like cache_control
@ -2253,7 +2245,7 @@ def drop_tool_reference_parts_from_tool_messages(
"""
if not any(_tool_message_carries_tool_reference(message) for message in messages):
return messages
return [_drop_tool_reference_parts(message) for message in messages] # mutable-ok: pipelines mutate message lists
return [_drop_tool_reference_parts(message) for message in messages]
def _attempt_json_repair(s: str) -> object | None:

View file

@ -243,28 +243,28 @@ def _inferred_format(file: Mapping[str, object], url: str) -> Mapping[str, str]:
def _inlined_image_url(image_url: Mapping[str, object] | None, data_url: str) -> Mapping[str, object] | str:
return {**image_url, "url": data_url} if image_url is not None else data_url # mutable-ok: json-serialized part
return {**image_url, "url": data_url} if image_url is not None else data_url
def _inlined_file(file: Mapping[str, object], url: str, data_url: str) -> Mapping[str, object]:
kept: Final = {k: v for k, v in file.items() if k != "file_id"} # mutable-ok: json-serialized message part
return {**kept, **_inferred_format(file, url), "file_data": data_url} # mutable-ok: json-serialized part
kept: Final = {k: v for k, v in file.items() if k != "file_id"}
return {**kept, **_inferred_format(file, url), "file_data": data_url}
def _base64_source(url: str, data_url: str) -> Mapping[str, str]:
fetched_media_type, data = data_url.removeprefix("data:").split(";base64,", 1)
media_type: Final = "application/pdf" if url.lower().endswith(".pdf") else fetched_media_type
return {"type": "base64", "media_type": media_type, "data": data} # mutable-ok: json-serialized message part
return {"type": "base64", "media_type": media_type, "data": data}
def _inline(remote: _RemoteImage | _RemoteFile | _RemoteSource, data_url: str) -> Mapping[str, object]:
match remote:
case _RemoteImage(part, image_url, _):
return {**part, "image_url": _inlined_image_url(image_url, data_url)} # mutable-ok: json-serialized part
return {**part, "image_url": _inlined_image_url(image_url, data_url)}
case _RemoteFile(part, file, url):
return {**part, "file": _inlined_file(file, url, data_url)} # mutable-ok: json-serialized message part
return {**part, "file": _inlined_file(file, url, data_url)}
case _RemoteSource(part, _, url):
return {**part, "source": _base64_source(url, data_url)} # mutable-ok: json-serialized message part
return {**part, "source": _base64_source(url, data_url)}
def _content_parts(message: Mapping[str, object]) -> tuple[object, ...]:
@ -286,10 +286,8 @@ def _inline_message(
parts: Final = _content_parts(message)
if not parts:
return message
inlined_parts: Final = [ # mutable-ok: content must stay a list for the transforms' isinstance checks
_inline_part(part, data_urls, should_inline) for part in parts
]
inlined_message: Final = {**message, "content": inlined_parts} # mutable-ok: json-serialized message
inlined_parts: Final = [_inline_part(part, data_urls, should_inline) for part in parts]
inlined_message: Final = {**message, "content": inlined_parts}
return inlined_message # pyright: ignore[reportReturnType] # the same message with its remote parts inlined
@ -326,6 +324,4 @@ async def async_inline_remote_media(
return messages
data_urls: Final = await _fetch_data_urls(remote_urls)
inlined: Final = MappingProxyType(dict(zip(remote_urls, data_urls, strict=True)))
return [ # mutable-ok: transform_request takes a list
_inline_message(message, inlined, should_inline) for message in messages
]
return [_inline_message(message, inlined, should_inline) for message in messages]

View file

@ -461,12 +461,8 @@ class ChunkProcessor:
def get_combined_tool_content(
self, tool_call_chunks: Sequence["_ToolCallChunk"]
) -> list[
ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall
]: # mutable-ok: assigned verbatim to Message.tool_calls, a list field
tool_calls_list: list[
ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall
] = [] # mutable-ok: see return type
) -> list[ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall]:
tool_calls_list: list[ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall] = []
tool_call_map: Final[dict[int, dict[str, Any]]] = {} # Map to store tool calls by index
for chunk in tool_call_chunks:

View file

@ -209,9 +209,7 @@ def _provider_hidden_params(
hidden: Final[object] = getattr(chunk, "_hidden_params", None)
parsed: Final = _parsed_provider_hidden_params(hidden)
provider_specific_fields: Final[object | None] = (
dict(parsed.provider_specific_fields) # mutable-ok: stream assembly merges provider metadata into this dict
if parsed is not None and parsed.provider_specific_fields
else None
dict(parsed.provider_specific_fields) if parsed is not None and parsed.provider_specific_fields else None
)
params: Final[Mapping[str, object]] = MappingProxyType(
{

View file

@ -179,7 +179,7 @@ def _rewritten_event(event: Mapping[str, object], rewrite_event: _SSEEventRewrit
section: Final = None if rewrite is None else event.get(rewrite.section)
if rewrite is None or not isinstance(section, Mapping):
return event
return {**event, rewrite.section: {**section, rewrite.field: rewrite.value}} # mutable-ok: json.dumps needs a dict
return {**event, rewrite.section: {**section, rewrite.field: rewrite.value}}
def _tool_call_shapes(tool_calls: Sequence[object]) -> tuple[_ToolCallShape, ...]:
@ -455,9 +455,7 @@ class AnthropicMessagesHandler(BaseTranslation):
# Exclude only the trusted top-level prompt. In-sequence system entries are untrusted
# and must stay aligned with texts_to_check for positional masking. When the top-level
# prompt is included, the pre-existing count mismatch disables positional masking.
translation_source: Final = { # mutable-ok: API message payload
key: value for key, value in data.items() if key != "system"
}
translation_source: Final = {key: value for key, value in data.items() if key != "system"}
chat_completion_compatible_request: Final = self._translate_to_openai(translation_source)
full_structured_messages: Final = cast(
@ -502,10 +500,8 @@ class AnthropicMessagesHandler(BaseTranslation):
for msg_idx, message in enumerate(messages)
)
scanned: Final = tuple(item for one_message in extracted for item in one_message.scanned)
texts_to_check: Final = [item.text for item in scanned] # mutable-ok: GenericGuardrailAPIInputs takes list[str]
images_to_check: Final = [
image for one_message in extracted for image in one_message.images
] # mutable-ok: GenericGuardrailAPIInputs takes list[str]
texts_to_check: Final = [item.text for item in scanned]
images_to_check: Final = [image for one_message in extracted for image in one_message.images]
# Step 2: Apply guardrail to all texts in batch
if texts_to_check:
@ -581,33 +577,29 @@ class AnthropicMessagesHandler(BaseTranslation):
return data
def _hoisted_top_level_system_message(
self, data: dict
) -> AllMessageValues | None: # mutable-ok: API message payload
def _hoisted_top_level_system_message(self, data: dict) -> AllMessageValues | None:
"""Return the system message produced by translating the top-level prompt."""
system: Final = data.get("system")
if not system:
return None
probe: Final = self._translate_to_openai(
{ # mutable-ok: API message payload
{
"model": data.get("model") or "",
"messages": [], # mutable-ok: API message payload
"messages": [],
"system": system,
}
)
hoisted: Final = probe.get("messages") or [] # mutable-ok: API message payload
hoisted: Final = probe.get("messages") or []
return hoisted[0] if hoisted else None
@staticmethod
def _openai_system_message_to_anthropic(
message: Mapping[str, object],
) -> dict[str, object] | None: # mutable-ok: API message payload
) -> dict[str, object] | None:
"""Convert an OpenAI system message to the client's Anthropic-shaped entry."""
content: Final = message.get("content")
if isinstance(content, str):
return (
{"role": "system", "content": content} if content else None # mutable-ok: API message payload
) # mutable-ok: API message payload
return {"role": "system", "content": content} if content else None
if not isinstance(content, list):
return None
blocks: Final[list[dict[str, object]]] = [] # mutable-ok: API message payload
@ -617,21 +609,19 @@ class AnthropicMessagesHandler(BaseTranslation):
text = block.get("text")
if not isinstance(text, str) or not text:
continue
anthropic_block: dict[str, object] = { # mutable-ok: API message payload
anthropic_block: dict[str, object] = {
"type": "text",
"text": text,
} # mutable-ok: API message payload
}
cache_control = block.get("cache_control")
if cache_control:
anthropic_block["cache_control"] = deepcopy(cache_control)
blocks.append(anthropic_block)
return (
{"role": "system", "content": blocks} if blocks else None # mutable-ok: API message payload
) # mutable-ok: API message payload
return {"role": "system", "content": blocks} if blocks else None
@staticmethod
def _fold_leading_systems_into_top_level(
data: dict[str, object], # mutable-ok: API message payload
data: dict[str, object],
leading_systems: Sequence[object],
include_existing_system: bool,
) -> None:
@ -712,7 +702,7 @@ class AnthropicMessagesHandler(BaseTranslation):
@staticmethod
def _write_back_structured_messages(
data: dict, # mutable-ok: API message payload
data: dict,
structured_messages: list, # mutable-ok: API message payload
hoisted_system_message: object = None,
preserve_system_messages: bool = False,
@ -731,7 +721,7 @@ class AnthropicMessagesHandler(BaseTranslation):
for group in group_tool_exchanges(run):
converted.extend(
anthropic_messages_pt(
messages=[run[index] for index in group], # mutable-ok: API message payload
messages=[run[index] for index in group],
model=model,
llm_provider="anthropic",
)
@ -957,24 +947,16 @@ class AnthropicMessagesHandler(BaseTranslation):
match target:
case MessageContentTarget():
if isinstance(content, str):
message["content"] = (
guardrail_response # mutable-ok: guardrails rewrite the caller's request payload in place
)
message["content"] = guardrail_response
case ContentBlockTextTarget(content_idx=content_idx):
if isinstance(content, list):
content[content_idx]["text"] = (
guardrail_response # mutable-ok: guardrails rewrite the caller's request payload in place
)
content[content_idx]["text"] = guardrail_response
case ToolResultStringTarget(content_idx=content_idx):
if isinstance(content, list):
content[content_idx]["content"] = (
guardrail_response # mutable-ok: guardrails rewrite the caller's request payload in place
)
content[content_idx]["content"] = guardrail_response
case ToolResultBlockTextTarget(content_idx=content_idx, block_idx=block_idx):
if isinstance(content, list):
content[content_idx]["content"][block_idx]["text"] = (
guardrail_response # mutable-ok: guardrails rewrite the caller's request payload in place
)
content[content_idx]["content"][block_idx]["text"] = guardrail_response
case _:
assert_never(target)

View file

@ -371,7 +371,7 @@ class AnthropicChatCompletion(BaseLLM):
transform_params: Final = {**optional_params, "is_vertex_request": is_vertex_request}
def finish_request(request_data: dict) -> tuple[dict, dict]: # mutable-ok: rewritten in place downstream
def finish_request(request_data: dict) -> tuple[dict, dict]:
"""Filter beta headers and emit pre_call, returning `(headers, data)`.
The pair stays mutable because the streaming path rewrites it in
@ -460,7 +460,7 @@ class AnthropicChatCompletion(BaseLLM):
# before transforming: whichever path runs emits pre_call exactly once.
# `get_config` merges the class-level defaults (Anthropic's required
# `max_tokens` among them) that `transform_request` would have applied.
rust_optional_params: Final = { # mutable-ok: json.dumps in the bridge rejects a mappingproxy
rust_optional_params: Final = {
**AnthropicConfig.get_config(model=model),
**optional_params,
}
@ -473,8 +473,8 @@ class AnthropicChatCompletion(BaseLLM):
stream=stream,
)
if serves_via_rust:
rust_logging_args: Final = { # mutable-ok: logging callbacks read additional_args as a plain dict
"complete_input_dict": { # mutable-ok: same, and it is serialized alongside its parent
rust_logging_args: Final = {
"complete_input_dict": {
"model": model,
"messages": messages,
**rust_optional_params,
@ -650,7 +650,7 @@ class ModelResponseIterator:
# Accumulate web_search_tool_result blocks for multi-turn reconstruction
# See: https://github.com/BerriAI/litellm/issues/17737
self.web_search_results: list[dict[str, object]] = []
self._web_search_calls: dict[str, object] = {} # mutable-ok: provider call state by id
self._web_search_calls: dict[str, object] = {}
# Accumulate compaction blocks for multi-turn reconstruction
self.compaction_blocks: list[dict[str, object]] = []
@ -825,7 +825,7 @@ class ModelResponseIterator:
return content_block_start
def _web_search_call_snapshot(self) -> dict[str, object]:
return dict(self._web_search_calls) # mutable-ok: stream payload snapshot
return dict(self._web_search_calls)
def _complete_web_search_call(self, result: dict[str, object]) -> None:
tool_use_id: Final = result.get("tool_use_id")
@ -833,7 +833,7 @@ class ModelResponseIterator:
return
self._web_search_calls[tool_use_id] = build_web_search_call(
tool_id=tool_use_id,
tool_input=self._server_tool_inputs.get(tool_use_id, {}), # mutable-ok: empty provider input
tool_input=self._server_tool_inputs.get(tool_use_id, {}),
result=result,
)
@ -942,7 +942,7 @@ class ModelResponseIterator:
self._web_search_calls[self._current_server_tool_id] = build_web_search_call(
self._current_server_tool_id,
tool_input,
{"content": []}, # mutable-ok: no provider result yet
{"content": []},
status="in_progress",
)
provider_specific_fields["web_search_calls"] = self._web_search_call_snapshot()

View file

@ -1853,10 +1853,10 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
self,
model: str,
messages: list[AllMessageValues], # mutable-ok: BaseConfig signature
optional_params: dict[str, object], # mutable-ok: BaseConfig signature
litellm_params: dict[str, object], # mutable-ok: BaseConfig signature
headers: dict[str, object], # mutable-ok: BaseConfig signature
) -> dict[str, object]: # mutable-ok: BaseConfig signature
optional_params: dict[str, object],
litellm_params: dict[str, object],
headers: dict[str, object],
) -> dict[str, object]:
return self.transform_request(
model=model,
messages=await async_inline_remote_media(messages, should_inline=self.inlines_remote_media),
@ -2067,7 +2067,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
optional_params.pop("output_config", None)
data.pop("output_config", None)
return
format_only: Final = {"format": preserved_format} # mutable-ok: json body
format_only: Final = {"format": preserved_format}
optional_params["output_config"] = format_only # rebind-ok: out-param store
data["output_config"] = format_only # rebind-ok: out-param store
return
@ -2472,7 +2472,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
) -> list[object]:
content: Final = completion_response.get("content")
blocks: Final = content if isinstance(content, Sequence) else ()
inputs: Final = { # mutable-ok: indexes provider server inputs
inputs: Final = {
call_id: tool_input
for block in blocks
if isinstance(block, Mapping)
@ -2481,10 +2481,10 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
and isinstance((call_id := block.get("id")), str)
and isinstance((tool_input := block.get("input")), Mapping)
}
return [ # mutable-ok: provider-neutral response items
return [
build_web_search_call(
tool_id=tool_use_id,
tool_input=inputs.get(tool_use_id, {}), # mutable-ok: empty provider input
tool_input=inputs.get(tool_use_id, {}),
result=result,
)
for result in web_search_results

View file

@ -618,7 +618,7 @@ class AnthropicModelInfo(BaseLLMModelInfo):
@staticmethod
def maybe_drop_disabled_thinking(
model: str,
optional_params: MutableMapping[str, object], # mutable-ok: in-place out-param, as in _maybe_drop_speed_param
optional_params: MutableMapping[str, object],
custom_llm_provider: str,
) -> None:
"""Omit ``thinking={'type': 'disabled'}`` for always-on-thinking models
@ -638,7 +638,7 @@ class AnthropicModelInfo(BaseLLMModelInfo):
@staticmethod
def translate_legacy_thinking_for_adaptive_model(
model: str,
optional_params: MutableMapping[str, object], # mutable-ok: in-place out-param like the sibling helpers
optional_params: MutableMapping[str, object],
custom_llm_provider: str,
) -> None:
"""Translate legacy ``thinking.type=enabled`` to adaptive for the
@ -1207,22 +1207,22 @@ def strip_thinking_blocks_from_anthropic_messages(messages: list[Any]) -> list[A
return out
def _without_encrypted_reasoning_blocks(message: dict) -> dict | None: # mutable-ok: Anthropic message payload shape
def _without_encrypted_reasoning_blocks(message: dict) -> dict | None:
if not isinstance(message, Mapping):
return message
content: Final = message.get("content")
if not isinstance(content, list):
return message
kept: Final = [b for b in content if not is_encrypted_reasoning_block(b)] # mutable-ok: API message payload
kept: Final = [b for b in content if not is_encrypted_reasoning_block(b)]
if len(kept) == len(content):
return message
if not kept:
return None
return {**message, "content": kept} # mutable-ok: API message payload
return {**message, "content": kept}
def strip_encrypted_reasoning_blocks_from_anthropic_messages(
messages: Sequence[dict], # mutable-ok: Anthropic message payload shape
messages: Sequence[dict],
) -> list[dict]: # mutable-ok: AnthropicMessagesRequest.messages is typed list[dict]
"""
Drop thinking / redacted_thinking blocks that carry another provider's encrypted
@ -1230,7 +1230,7 @@ def strip_encrypted_reasoning_blocks_from_anthropic_messages(
Anthropic, which cannot verify them. Anthropic's own signed blocks are kept.
"""
stripped: Final = (_without_encrypted_reasoning_blocks(m) for m in messages)
return [m for m in stripped if m is not None] # mutable-ok: API message payload
return [m for m in stripped if m is not None]
def strip_thinking_blocks_from_anthropic_messages_request_dict(
@ -1512,10 +1512,10 @@ def _flatten_web_search_results_in_message(message: object) -> object:
}
)
rewritten: Final = tuple(_rewrite_replayed_web_search_block(block, flattenable, queries) for block in content)
return {**message, "content": [b for b in rewritten if b is not None]} # mutable-ok: JSON wire format
return {**message, "content": [b for b in rewritten if b is not None]}
def flatten_unencrypted_web_search_results_in_anthropic_messages( # mutable-ok: as sibling sanitizers
def flatten_unencrypted_web_search_results_in_anthropic_messages(
messages: list[Any],
) -> list[Any]:
"""
@ -1530,49 +1530,47 @@ def flatten_unencrypted_web_search_results_in_anthropic_messages( # mutable-ok:
evidence in the conversation instead of 400ing the follow-up turn, and leaves
genuine Anthropic-issued blocks untouched.
"""
return [_flatten_web_search_results_in_message(m) for m in messages] # mutable-ok: JSON wire format
return [_flatten_web_search_results_in_message(m) for m in messages]
def _without_provider_specific_fields(block: object) -> object:
if not isinstance(block, dict) or "provider_specific_fields" not in block:
return block
return {k: v for k, v in block.items() if k != "provider_specific_fields"} # mutable-ok: JSON wire format
return {k: v for k, v in block.items() if k != "provider_specific_fields"}
def _strip_provider_specific_fields_in_message(message: object) -> object:
if not isinstance(message, dict) or not isinstance(message.get("content"), list):
return message
content: Final = [_without_provider_specific_fields(b) for b in message["content"]] # mutable-ok: JSON wire format
return {**message, "content": content} # mutable-ok: JSON wire format
content: Final = [_without_provider_specific_fields(b) for b in message["content"]]
return {**message, "content": content}
def strip_provider_specific_fields_from_anthropic_messages(
messages: Sequence[object],
) -> Sequence[object]:
return [_strip_provider_specific_fields_in_message(m) for m in messages] # mutable-ok: JSON wire format
return [_strip_provider_specific_fields_in_message(m) for m in messages]
def _normalized_cache_control(cache_control: object) -> dict[str, str] | None: # mutable-ok: JSON wire format
def _normalized_cache_control(cache_control: object) -> dict[str, str] | None:
if not isinstance(cache_control, Mapping):
return None
cache_type: Final = cache_control.get("type")
return {"type": cache_type if isinstance(cache_type, str) else "ephemeral"} # mutable-ok: JSON wire format
return {"type": cache_type if isinstance(cache_type, str) else "ephemeral"}
def _with_portable_cache_control(block: Mapping[str, object]) -> dict[str, object]: # mutable-ok: JSON wire format
def _with_portable_cache_control(block: Mapping[str, object]) -> dict[str, object]:
if "cache_control" not in block:
return dict(block) # mutable-ok: JSON wire format
return dict(block)
normalized: Final = _normalized_cache_control(block["cache_control"])
rest: Final = {key: value for key, value in block.items() if key != "cache_control"} # mutable-ok: JSON wire format
return rest if normalized is None else {**rest, "cache_control": normalized} # mutable-ok: JSON wire format
rest: Final = {key: value for key, value in block.items() if key != "cache_control"}
return rest if normalized is None else {**rest, "cache_control": normalized}
def _with_portable_cache_control_in_blocks(blocks: object) -> object:
if isinstance(blocks, str) or not isinstance(blocks, Sequence):
return blocks
return [ # mutable-ok: JSON wire format
_with_portable_cache_control(block) if isinstance(block, Mapping) else block for block in blocks
]
return [_with_portable_cache_control(block) if isinstance(block, Mapping) else block for block in blocks]
def _with_portable_cache_control_in_content_block(block: object) -> object:
@ -1581,7 +1579,7 @@ def _with_portable_cache_control_in_content_block(block: object) -> object:
portable: Final = _with_portable_cache_control(block)
if portable.get("type") != "tool_result" or "content" not in portable:
return portable
return { # mutable-ok: JSON wire format
return {
**portable,
"content": _with_portable_cache_control_in_blocks(portable["content"]),
}
@ -1593,20 +1591,16 @@ def _with_portable_cache_control_in_message(message: object) -> object:
content: Final = message["content"]
if isinstance(content, str) or not isinstance(content, Sequence):
return message
return { # mutable-ok: JSON wire format
return {
**message,
"content": [ # mutable-ok: JSON wire format
_with_portable_cache_control_in_content_block(block) for block in content
],
"content": [_with_portable_cache_control_in_content_block(block) for block in content],
}
def _with_portable_cache_control_in_messages(messages: object) -> object:
if isinstance(messages, str) or not isinstance(messages, Sequence):
return messages
return [ # mutable-ok: JSON wire format
_with_portable_cache_control_in_message(message) for message in messages
]
return [_with_portable_cache_control_in_message(message) for message in messages]
def _with_portable_cache_control_in_scoped_value(key: str, value: object) -> object:
@ -1621,7 +1615,7 @@ def _with_portable_cache_control_in_scoped_value(key: str, value: object) -> obj
def normalize_cache_control_in_anthropic_payload(
payload: Mapping[str, object],
) -> dict[str, object]: # mutable-ok: JSON wire format
) -> dict[str, object]:
"""
Return a copy of an Anthropic /v1/messages payload with every
``cache_control`` entry reduced to ``{"type": <its type, or "ephemeral">}``
@ -1638,9 +1632,7 @@ def normalize_cache_control_in_anthropic_payload(
dropped entirely. The caller's payload is never mutated.
"""
portable: Final = _with_portable_cache_control(payload)
return { # mutable-ok: JSON wire format
key: _with_portable_cache_control_in_scoped_value(key, value) for key, value in portable.items()
}
return {key: _with_portable_cache_control_in_scoped_value(key, value) for key, value in portable.items()}
def process_anthropic_headers(headers: httpx.Headers | dict) -> dict:
@ -1667,7 +1659,7 @@ def _anthropic_model_entry(
source: Final[Mapping[str, object]] = (
MappingProxyType({"source_model": model["id"]}) if listed_id is not None else MappingProxyType({})
)
return { # mutable-ok: JSON response body, serialized by the route and never mutated
return {
"type": "model",
"id": listed_id or model["id"],
**source,
@ -1698,10 +1690,8 @@ def create_anthropic_model_list_response(
created_at: Final = (
datetime.fromtimestamp(DEFAULT_MODEL_CREATED_AT_TIME, tz=timezone.utc).isoformat().replace("+00:00", "Z")
)
data: Final = [ # mutable-ok: JSON response body, serialized by the route and never mutated
_anthropic_model_entry(model, created_at, display_names, listed_ids) for model in models
]
return { # mutable-ok: JSON response body, serialized by the route and never mutated
data: Final = [_anthropic_model_entry(model, created_at, display_names, listed_ids) for model in models]
return {
"data": data,
"has_more": False,
"first_id": data[0]["id"] if data else None,

View file

@ -1041,9 +1041,9 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper):
return cast( # cast-ok: rebuilt dict matches the message_delta TypedDict shape for this branch
ContentBlockDelta | MessageBlockDelta,
{ # mutable-ok: fresh translation payload; never mutated after construction
{
**processed_chunk,
"delta": { # mutable-ok: fresh message_delta payload; never mutated after construction
"delta": {
**delta,
"stop_reason": "refusal",
"stop_details": refusal_stop_details(self._refusal_text),

View file

@ -1249,7 +1249,7 @@ class LiteLLMAnthropicMessagesAdapter:
case ({"type": "text", "text": str(text)},):
return text
case _:
return list(parts) # mutable-ok: content must be a json list
return list(parts)
def _tool_result_part(self, item: object) -> ToolMessageContentPart | None:
if isinstance(item, str):

View file

@ -42,7 +42,7 @@ class AnthropicMessagesStreamCacheWriter:
self.caching_handler = caching_handler
self.collected_chunks: list[bytes] = [] # mutable-ok: rebuilding a tuple per SSE chunk is quadratic
self.persisted = False
self._hidden_params: dict[str, object] = dict( # mutable-ok: callers stamp cache_key in here
self._hidden_params: dict[str, object] = dict(
stream._hidden_params if isinstance(stream, AnthropicMessagesStreamingResponse) else _EMPTY_MAPPING
)
@ -88,9 +88,7 @@ class AnthropicMessagesStreamCacheWriter:
try:
events: Final = _split_sse_events(collected_stream.decode("utf-8"))
cached_payload: Final = {
CACHED_STREAM_EVENTS_KEY: events
} # mutable-ok: cache backends serialize plain dicts
cached_payload: Final = {CACHED_STREAM_EVENTS_KEY: events}
await litellm.cache.async_add_cache(
cached_payload,
dynamic_cache_object=self.caching_handler.dual_cache,
@ -107,12 +105,12 @@ class CachedAnthropicMessagesStreamIterator(BaseAnthropicMessagesStreamingIterat
litellm_logging_obj: "LiteLLMLoggingObj",
request_body: Mapping[str, object],
) -> None:
body: Final = dict(request_body) # mutable-ok: the base iterator takes a plain dict
body: Final = dict(request_body)
super().__init__(litellm_logging_obj=litellm_logging_obj, request_body=body)
self.chunks: Final[tuple[bytes, ...]] = tuple(event.encode("utf-8") for event in events)
self.current_index = 0
self.logged = False
self._hidden_params: dict[str, object] = {"cache_hit": True} # mutable-ok: callers stamp cache_key in here
self._hidden_params: dict[str, object] = {"cache_hit": True}
litellm_logging_obj.model_call_details["cache_hit"] = True
def __aiter__(self) -> "CachedAnthropicMessagesStreamIterator":
@ -122,7 +120,7 @@ class CachedAnthropicMessagesStreamIterator(BaseAnthropicMessagesStreamingIterat
if self.current_index >= len(self.chunks):
if not self.logged:
self.logged = True
chunks: Final = list(self.chunks) # mutable-ok: the logging handler takes a list
chunks: Final = list(self.chunks)
await self._handle_streaming_logging(chunks)
raise StopAsyncIteration
chunk: Final = self.chunks[self.current_index]

View file

@ -186,7 +186,7 @@ def _sse_event(event_type: str, payload: Mapping[str, object]) -> bytes:
def _incomplete_stream_error_sse_event() -> bytes:
return _sse_event( # mutable-ok: one-shot JSON payload, never mutated after construction
return _sse_event(
"error",
{"type": "error", "error": {"type": "api_error", "message": INCOMPLETE_STREAM_ERROR_MESSAGE}},
)
@ -207,15 +207,15 @@ def _anthropic_content_block_start_and_deltas(
match block.get("type"):
case "tool_use":
return (
{ # mutable-ok: one-shot payload
{
"id": block.get("id"),
"name": block.get("name"),
"input": {}, # mutable-ok: one-shot payload
"input": {},
"type": "tool_use",
},
(
{ # mutable-ok: one-shot payload
"partial_json": json.dumps(block.get("input") or {}), # mutable-ok: one-shot payload
{
"partial_json": json.dumps(block.get("input") or {}),
"type": "input_json_delta",
},
),
@ -223,23 +223,23 @@ def _anthropic_content_block_start_and_deltas(
case "thinking":
signature: Final = block.get("signature")
signature_deltas: Final = (
({"signature": signature, "type": "signature_delta"},) # mutable-ok: one-shot payload
({"signature": signature, "type": "signature_delta"},)
if isinstance(signature, str) and signature
else ()
)
return (
{"thinking": "", "signature": "", "type": "thinking"}, # mutable-ok: one-shot payload
{"thinking": "", "signature": "", "type": "thinking"},
(
{"thinking": block.get("thinking") or "", "type": "thinking_delta"}, # mutable-ok: one-shot payload
{"thinking": block.get("thinking") or "", "type": "thinking_delta"},
*signature_deltas,
),
)
case "redacted_thinking":
return ({"type": "redacted_thinking", "data": block.get("data")}, ()) # mutable-ok: one-shot JSON payload
return ({"type": "redacted_thinking", "data": block.get("data")}, ())
case _:
return (
{"type": "text", "text": ""}, # mutable-ok: one-shot JSON payload
({"type": "text_delta", "text": block.get("text") or ""},), # mutable-ok: one-shot JSON payload
{"type": "text", "text": ""},
({"type": "text_delta", "text": block.get("text") or ""},),
)
@ -263,51 +263,51 @@ def anthropic_messages_response_as_sse_events(response: AnthropicMessagesRespons
# a zero output_tokens - those are only known once generation finishes, so
# copying the completed response's final values here would let a client
# treat the message as already finished, or double-count output tokens.
message_start_usage: Final = { # mutable-ok: one-shot JSON payload
message_start_usage: Final = {
**(response.get("usage") or {}),
"output_tokens": 0,
}
message_start_payload: Final = { # mutable-ok: one-shot JSON payload, never mutated after construction
message_start_payload: Final = {
"type": "message_start",
"message": { # mutable-ok: one-shot JSON payload
"message": {
**response,
"content": [], # mutable-ok: one-shot JSON payload
"content": [],
"stop_reason": None,
"stop_sequence": None,
"usage": message_start_usage,
},
}
message_delta_payload: Final = { # mutable-ok: one-shot JSON payload, never mutated after construction
message_delta_payload: Final = {
"type": "message_delta",
"delta": { # mutable-ok: one-shot JSON payload
"delta": {
"stop_reason": response.get("stop_reason"),
"stop_sequence": response.get("stop_sequence"),
},
"usage": response.get("usage") or {}, # mutable-ok: one-shot JSON payload
"usage": response.get("usage") or {},
}
return (
_sse_event("message_start", message_start_payload),
*content_events,
_sse_event("message_delta", message_delta_payload),
_sse_event("message_stop", {"type": "message_stop"}), # mutable-ok: one-shot JSON payload
_sse_event("message_stop", {"type": "message_stop"}),
)
def _anthropic_content_block_events(index: int, block: Mapping[str, object]) -> tuple[bytes, ...]:
start_block, deltas = _anthropic_content_block_start_and_deltas(block)
start_payload: Final = { # mutable-ok: one-shot payload
start_payload: Final = {
"type": "content_block_start",
"index": index,
"content_block": start_block,
}
stop_payload: Final = { # mutable-ok: one-shot payload
stop_payload: Final = {
"type": "content_block_stop",
"index": index,
}
delta_events: Final = tuple(
_sse_event(
"content_block_delta",
{"type": "content_block_delta", "index": index, "delta": delta}, # mutable-ok: one-shot payload
{"type": "content_block_delta", "index": index, "delta": delta},
)
for delta in deltas
)

View file

@ -118,20 +118,20 @@ class AnthropicResponsesStreamWrapper:
if block_idx < 0:
redacted_idx: Final = self._open_block(
item_id,
{"type": "redacted_thinking", "data": signature}, # mutable-ok: API message payload
{"type": "redacted_thinking", "data": signature},
)
stop: Final = {"type": "content_block_stop", "index": redacted_idx} # mutable-ok: API message payload
stop: Final = {"type": "content_block_stop", "index": redacted_idx}
self._chunk_queue.append(stop)
return
if signature is not None:
self._chunk_queue.append(
{ # mutable-ok: API message payload
{
"type": "content_block_delta",
"index": block_idx,
"delta": {"type": "signature_delta", "signature": signature}, # mutable-ok: API message payload
"delta": {"type": "signature_delta", "signature": signature},
}
)
self._chunk_queue.append({"type": "content_block_stop", "index": block_idx}) # mutable-ok: API message payload
self._chunk_queue.append({"type": "content_block_stop", "index": block_idx})
def _process_event(self, event: object) -> None:
"""Convert one Responses API event into zero or more Anthropic chunks queued for emission."""
@ -223,10 +223,10 @@ class AnthropicResponsesStreamWrapper:
if part_block_idx < 0 or not isinstance(summary_index, int) or summary_index == 0:
return
self._chunk_queue.append(
{ # mutable-ok: API message payload
{
"type": "content_block_delta",
"index": part_block_idx,
"delta": { # mutable-ok: API message payload
"delta": {
"type": "thinking_delta",
"thinking": REASONING_SUMMARY_PART_SEPARATOR,
},
@ -244,7 +244,7 @@ class AnthropicResponsesStreamWrapper:
return
block_idx = self._open_block(
item_id,
{"type": "thinking", "thinking": "", "signature": ""}, # mutable-ok: API message payload
{"type": "thinking", "thinking": "", "signature": ""},
)
self._chunk_queue.append(
{
@ -327,16 +327,10 @@ class AnthropicResponsesStreamWrapper:
else AnthropicUsage(input_tokens=0, output_tokens=0)
)
message_delta_payload: Final = { # mutable-ok: fresh message_delta payload built per chunk
message_delta_payload: Final = {
"stop_reason": stop_reason,
"stop_sequence": None,
**(
{ # mutable-ok: fresh message_delta stop_details entry built per chunk
"stop_details": refusal_stop_details(refusal_text)
}
if stop_reason == "refusal"
else {} # mutable-ok: empty spread placeholder for non-refusal stop
),
**({"stop_details": refusal_stop_details(refusal_text)} if stop_reason == "refusal" else {}),
}
self._chunk_queue.append(

View file

@ -98,7 +98,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
@staticmethod
def _translate_anthropic_document_block_to_file_part(
block: Mapping[str, object],
) -> dict[str, str] | None: # mutable-ok: API message payload
) -> dict[str, str] | None:
"""Convert an Anthropic document block to a Responses input_file part."""
raw_source: Final = block.get("source")
if not isinstance(raw_source, Mapping):
@ -115,7 +115,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
)
raw_title: Final = block.get("title")
filename: Final = raw_title if isinstance(raw_title, str) and raw_title else "document.pdf"
return { # mutable-ok: API message payload
return {
"type": "input_file",
"filename": filename,
"file_data": f"data:{media_type};base64,{data}",
@ -124,21 +124,19 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
url: Final = source.get("url")
if not isinstance(url, str) or not url:
return None
return {"type": "input_file", "file_url": url} # mutable-ok: API message payload
return {"type": "input_file", "file_url": url}
return None
@staticmethod
def _tool_result_output_value(
output_text: str,
file_parts: tuple[dict[str, str], ...], # mutable-ok: json content parts
file_parts: tuple[dict[str, str], ...],
) -> str | list[dict[str, str]]: # mutable-ok: API message payload
"""Plain string output, or a part list when document file parts are present."""
if not file_parts:
return output_text
text_parts: Final = (
[{"type": "input_text", "text": output_text}] if output_text else [] # mutable-ok: API message payload
)
return [*text_parts, *file_parts] # mutable-ok: API message payload
text_parts: Final = [{"type": "input_text", "text": output_text}] if output_text else []
return [*text_parts, *file_parts]
@staticmethod
def _translate_midturn_system_content_to_responses(
@ -146,15 +144,11 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
) -> list[dict[str, object]]: # mutable-ok: API message payload
"""Convert in-sequence system content to Responses input-text parts."""
if isinstance(content, str):
return (
[{"type": "input_text", "text": content}] if content else [] # mutable-ok: API message payload
) # mutable-ok: API message payload
return [{"type": "input_text", "text": content}] if content else []
if not isinstance(content, list):
return [] # mutable-ok: API message payload
return [ # mutable-ok: API message payload
with_prompt_cache_breakpoint(
{"type": "input_text", "text": text}, block.get("prompt_cache_breakpoint")
) # mutable-ok: API message payload
return []
return [
with_prompt_cache_breakpoint({"type": "input_text", "text": text}, block.get("prompt_cache_breakpoint"))
for block in content
if isinstance(block, dict) and block.get("type") == "text" and (text := block.get("text")) # pyright: ignore[reportUnnecessaryIsInstance] # untrusted client payload
]
@ -171,7 +165,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
cls,
summary: Iterable[object],
encrypted_content: object,
) -> dict[str, Any] | None: # mutable-ok: API message payload
) -> dict[str, Any] | None:
"""The one Anthropic block for a Responses reasoning item.
The item's encrypted reasoning rides the block's opaque field (`signature`, or
@ -198,21 +192,19 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
return "thinking" if block.get("type") in ("thinking", "redacted_thinking") else f"block:{index}"
@classmethod
def _assistant_group_to_input_items(
cls, group: tuple[Mapping[str, object], ...]
) -> tuple[dict[str, Any], ...]: # mutable-ok: API message payload
def _assistant_group_to_input_items(cls, group: tuple[Mapping[str, object], ...]) -> tuple[dict[str, Any], ...]:
first: Final = group[0]
btype: Final = first.get("type")
if btype in ("thinking", "redacted_thinking"):
replayed: Final = responses_reasoning_items_from_thinking_blocks(group)
return tuple(dict(item) for item in replayed) # mutable-ok: API message payload
return tuple(dict(item) for item in replayed)
if btype == "tool_use":
return (
{ # mutable-ok: API message payload
{
"type": "function_call",
"call_id": first.get("id", ""),
"name": first.get("name", ""),
"arguments": json.dumps(first.get("input", {})), # mutable-ok: API message payload
"arguments": json.dumps(first.get("input", {})),
},
)
return ()
@ -241,7 +233,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
system_parts = self._translate_midturn_system_content_to_responses(m.get("content"))
if system_parts:
input_items.append(
{ # mutable-ok: API message payload
{
"type": "message",
"role": "system",
"content": system_parts,
@ -324,8 +316,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
else TOOL_RESULT_IMAGE_PLACEHOLDER
)
tool_image_parts.extend(
{"type": "input_image", "image_url": url} # mutable-ok: json content part
for url in image_urls
{"type": "input_image", "image_url": url} for url in image_urls
)
else:
output_text = str(inner)
@ -338,15 +329,15 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
}
)
if tool_image_parts:
boundary_part = { # mutable-ok: json content part
boundary_part = {
"type": "input_text",
"text": TOOL_RESULT_IMAGE_BOUNDARY,
}
input_items.append(
{ # mutable-ok: json input item
{
"type": "message",
"role": "user",
"content": [boundary_part, *tool_image_parts], # mutable-ok: json content list
"content": [boundary_part, *tool_image_parts],
}
)
if user_parts:
@ -375,7 +366,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
for item in self._assistant_group_to_input_items(tuple(block for _, block in group))
)
asst_parts: list[dict[str, Any]] = [ # mutable-ok: API message payload
{"type": "output_text", "text": block.get("text", "")} # mutable-ok: API message payload
{"type": "output_text", "text": block.get("text", "")}
for block in blocks
if block.get("type") == "text"
]
@ -533,7 +524,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
if developer_parts:
input_items.insert(
0,
{ # mutable-ok: API message payload
{
"type": "message",
"role": "developer",
"content": developer_parts,
@ -545,7 +536,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
"input": input_items,
}
if include_encrypted_reasoning:
responses_kwargs["include"] = [RESPONSES_INCLUDE_ENCRYPTED_REASONING] # mutable-ok: API request payload
responses_kwargs["include"] = [RESPONSES_INCLUDE_ENCRYPTED_REASONING]
if system and not developer_parts:
if isinstance(system, str):

View file

@ -1407,7 +1407,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM):
logging_obj.pre_call(
input=input,
api_key=api_key,
additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict
additional_args={
"complete_input_dict": speech_request_body(model, voice, optional_params),
"api_base": str(azure_client.base_url),
},
@ -1451,7 +1451,7 @@ class AzureChatCompletion(BaseAzureLLM, BaseLLM):
logging_obj.pre_call(
input=input,
api_key=api_key,
additional_args={ # mutable-ok: loggers isinstance-check this payload as a dict
additional_args={
"complete_input_dict": speech_request_body(model, voice, optional_params),
"api_base": str(azure_client.base_url),
},

View file

@ -44,7 +44,7 @@ def sanitized_tools_update(optional_params: Mapping[str, object]) -> Mapping[str
tools: Final = optional_params.get("tools")
if not isinstance(tools, list):
return _NO_TOOLS_UPDATE
sanitized: Final = [ # mutable-ok: request tools are a JSON list
sanitized: Final = [
tool_with_sanitized_parameters(tool, flatten_combinators_and_drop_non_python_regex_patterns)
if isinstance(tool, dict)
else tool

View file

@ -109,7 +109,7 @@ class AzureOpenAIO1Config(OpenAIOSeriesConfig):
headers: dict,
) -> dict:
model = model.replace("o_series/", "") # handle o_series/my-random-deployment-name
flattened_params: Final = { # mutable-ok: transform_request's contract takes a plain JSON params dict
flattened_params: Final = {
**optional_params,
**sanitized_tools_update(optional_params),
}

View file

@ -278,18 +278,18 @@ class BingGroundingSearchConfig(BaseSearchConfig):
def validate_environment(
self,
headers: dict[str, str], # mutable-ok: BaseSearchConfig.validate_environment signature
headers: dict[str, str],
api_key: str | None = None,
api_base: str | None = None,
**kwargs: object, # kwargs-ok: BaseSearchConfig.validate_environment signature
) -> dict[str, str]: # mutable-ok: the http handler passes this straight to httpx as headers
) -> dict[str, str]:
"""
Validate environment and return headers.
Returns a new dict rather than mutating ``headers``: the http handler calls this
a second time after ``litellm/search/main.py`` already did, so it has to be idempotent.
"""
return { # mutable-ok: httpx requires a plain dict of headers
return {
**headers,
**self._auth_header(api_key, api_base),
"Content-Type": "application/json",
@ -330,7 +330,7 @@ class BingGroundingSearchConfig(BaseSearchConfig):
def get_complete_url(
self,
api_base: str | None,
optional_params: dict[str, object], # mutable-ok: BaseSearchConfig.get_complete_url signature
optional_params: dict[str, object],
data: dict[str, object] | list[dict[str, object]] | None = None, # mutable-ok: base signature
**kwargs: object, # kwargs-ok: BaseSearchConfig.get_complete_url signature
) -> str:
@ -348,9 +348,9 @@ class BingGroundingSearchConfig(BaseSearchConfig):
def transform_search_request(
self,
query: str | list[str], # mutable-ok: BaseSearchConfig.transform_search_request signature
optional_params: dict[str, object], # mutable-ok: base signature
optional_params: dict[str, object],
**kwargs: object, # kwargs-ok: BaseSearchConfig.transform_search_request signature
) -> dict[str, object]: # mutable-ok: the http handler passes this straight to httpx as the JSON body
) -> dict[str, object]:
"""
Transform Search request to the Foundry Responses API format.
@ -387,7 +387,7 @@ class BingGroundingSearchConfig(BaseSearchConfig):
raise self.get_error_class(
error_message=f"response does not match the Foundry Responses API schema: {e}",
status_code=raw_response.status_code,
headers=dict(raw_response.headers), # mutable-ok: BaseSearchConfig.get_error_class signature
headers=dict(raw_response.headers),
)
if parsed.status == "failed":
detail: Final = (
@ -408,7 +408,7 @@ class BingGroundingSearchConfig(BaseSearchConfig):
return self.get_error_class(
error_message=detail,
status_code=_UPSTREAM_ERROR_STATUS,
headers=dict(raw_response.headers), # mutable-ok: BaseSearchConfig.get_error_class signature
headers=dict(raw_response.headers),
)
def _priced(self, results: tuple[SearchResult, ...]) -> SearchResponse:
@ -416,23 +416,19 @@ class BingGroundingSearchConfig(BaseSearchConfig):
inherit the connection-mode ``bing_grounding/search`` price; zero its per-query
cost while leaving connection mode to the cost map."""
response: Final = SearchResponse(
results=list(results), # mutable-ok: SearchResponse.results is list[SearchResult]
results=list(results),
object="search",
)
if get_secret_str(CONNECTION_ID_ENV):
return response
response._hidden_params[
"additional_headers"
] = { # mutable-ok: response_cost_calculator writes into _hidden_params
_RESPONSE_COST_HEADER: 0.0
}
response._hidden_params["additional_headers"] = {_RESPONSE_COST_HEADER: 0.0}
return response
def get_error_class(
self,
error_message: str,
status_code: int,
headers: dict[str, str], # mutable-ok: BaseSearchConfig.get_error_class signature
headers: dict[str, str],
) -> Exception:
detail: Final = _unwrap_error_detail(error_message).rstrip(". ")
return BaseLLMException(

View file

@ -102,7 +102,7 @@ class AzureModelRouterConfig(AzureAIStudioConfig):
if selected_model:
# Rebuilt rather than mutated in place: ModelResponseBase declares _hidden_params as a
# class-level dict, so an in-place write can bleed into unrelated responses.
transformed_response._hidden_params = { # pyright: ignore[reportPrivateUsage] # ModelResponse exposes no public hidden-params setter # mutable-ok: ModelResponse requires _hidden_params to be a plain dict
transformed_response._hidden_params = { # pyright: ignore[reportPrivateUsage] # ModelResponse exposes no public hidden-params setter
**get_hidden_params_dict(transformed_response),
AZURE_MODEL_ROUTER_SELECTED_MODEL_KEY: selected_model,
}

View file

@ -91,11 +91,11 @@ class AzureAIStudioConfig(OpenAIConfig):
def map_openai_params(
self,
non_default_params: dict[str, object], # mutable-ok: OpenAIConfig.map_openai_params signature
optional_params: dict[str, object], # mutable-ok: OpenAIConfig.map_openai_params signature
non_default_params: dict[str, object],
optional_params: dict[str, object],
model: str,
drop_params: bool,
) -> dict[str, object]: # mutable-ok: OpenAIConfig.map_openai_params signature
) -> dict[str, object]:
if not azureAIGPT5Config.is_model_gpt_5_model(model):
return super().map_openai_params(
non_default_params=non_default_params,

View file

@ -39,7 +39,7 @@ class AzureAICohereParseConfig(CohereParseConfig):
api_base: str | None = None,
litellm_params: Mapping[str, object] | None = None,
**kwargs: object, # kwargs-ok: BaseOCRConfig.validate_environment signature
) -> dict[str, str]: # mutable-ok: BaseOCRConfig signature
) -> dict[str, str]:
resolved_base: Final = api_base or get_secret_str(AZURE_AI_API_BASE_ENV_VAR)
if resolved_base is None:
raise ValueError(
@ -47,7 +47,7 @@ class AzureAICohereParseConfig(CohereParseConfig):
"or pass api_base parameter"
)
resolved_key: Final = api_key or get_secret_str(AZURE_AI_API_KEY_ENV_VAR)
return { # mutable-ok: BaseOCRConfig signature
return {
**get_azure_ai_auth_headers(api_key=resolved_key, litellm_params=litellm_params),
"Content-Type": "application/json",
**headers,

View file

@ -147,13 +147,13 @@ class AzureAIPassthroughConfig(AzureFoundryModelInfo, BasePassthroughConfig):
litellm_params: Mapping[str, object],
api_key: str | None = None,
api_base: str | None = None,
) -> dict[str, str]: # mutable-ok: base class contract returns dict for httpx
) -> dict[str, str]:
auth_headers: Final = get_azure_ai_auth_headers(
api_key=api_key,
litellm_params=litellm_params,
api_key_header=api_key_header_for_base(api_base),
)
return {**headers, **auth_headers} # mutable-ok: base class contract returns dict for httpx
return {**headers, **auth_headers}
def logging_non_streaming_response(
self,
@ -170,7 +170,7 @@ class AzureAIPassthroughConfig(AzureFoundryModelInfo, BasePassthroughConfig):
model=model,
custom_llm_provider=custom_llm_provider,
httpx_response=httpx_response,
request_data=dict(request_data), # mutable-ok: AzurePassthroughConfig wants a dict
request_data=dict(request_data),
logging_obj=logging_obj,
endpoint=endpoint,
)
@ -196,7 +196,7 @@ class AzureAIPassthroughConfig(AzureFoundryModelInfo, BasePassthroughConfig):
ocr_config.get_complete_url(
api_base=relayed_origin,
model=model,
optional_params={}, # mutable-ok: BaseOCRConfig wants a dict
optional_params={},
)
)
known_prefixes: Final = (model, model_group_from(logging_obj.litellm_params))

View file

@ -164,7 +164,7 @@ class BaseFilesConfig(BaseConfig):
self,
raw_response: httpx.Response,
optional_params: Mapping[str, object],
litellm_params: dict, # mutable-ok: carries provider stashes from the request transform to the response one
litellm_params: dict,
) -> tuple[str, dict[str, str]] | None:
"""Request for the page after `raw_response`, or None once the listing is complete."""
return None

View file

@ -117,7 +117,7 @@ class BaseTranslation(ABC):
@staticmethod
def merge_user_api_key_metadata_into_request(
request_data: dict[str, Any], # mutable-ok: proxy hooks share and mutate the request payload dict in place
request_data: dict[str, Any],
user_api_key_dict: Optional["UserAPIKeyAuth"],
) -> None:
"""

View file

@ -155,7 +155,7 @@ class BaseOCRConfig:
return dynamic_api_key or api_key, dynamic_api_base or api_base
def get_health_check_document(self) -> DocumentType:
return { # mutable-ok: litellm.aocr rejects any document that is not a dict
return {
"type": "document_url",
"document_url": HEALTH_CHECK_PDF_DATA_URI,
}

View file

@ -185,12 +185,12 @@ class BaseSearchConfig:
def sign_request(
self,
headers: dict[str, str], # mutable-ok: matches the request header dict every other hook on this base takes
optional_params: dict[str, object], # mutable-ok: matches every other hook on this base
headers: dict[str, str],
optional_params: dict[str, object],
request_data: dict[str, object] | list[dict[str, object]], # mutable-ok: transform_search_request's body
api_base: str,
api_key: str | None = None,
) -> tuple[dict[str, str], bytes | None]: # mutable-ok: the handler passes these headers straight to httpx
) -> tuple[dict[str, str], bytes | None]:
"""
OPTIONAL
@ -268,7 +268,7 @@ class BaseSearchConfig:
return self.get_error_class(
error_message=error.response.text,
status_code=error.response.status_code,
headers=dict(error.response.headers), # mutable-ok: provider error factories require dict headers
headers=dict(error.response.headers),
)
def get_error_class(

View file

@ -48,8 +48,8 @@ class LiteLLMVectorStoreEmbeddingExecutor:
return litellm.embedding( # pyright: ignore[reportCallIssue, reportUnknownMemberType, reportUnknownVariableType] # provider kwargs are intentionally dynamic
model=model,
input=[query], # mutable-ok: LiteLLM embedding requires a mutable input list
**dict(configuration), # pyright: ignore[reportArgumentType] # provider-specific embedding config is validated downstream # mutable-ok: kwargs require a concrete dict
input=[query],
**dict(configuration), # pyright: ignore[reportArgumentType] # provider-specific embedding config is validated downstream
)
async def aembed(self, model: str, query: str, configuration: Mapping[str, object]) -> EmbeddingResponse:
@ -57,8 +57,8 @@ class LiteLLMVectorStoreEmbeddingExecutor:
return await litellm.aembedding( # pyright: ignore[reportUnknownMemberType] # provider kwargs are intentionally dynamic
model=model,
input=[query], # mutable-ok: LiteLLM embedding requires a mutable input list
**dict(configuration), # pyright: ignore[reportArgumentType] # provider-specific embedding config is validated downstream # mutable-ok: kwargs require a concrete dict
input=[query],
**dict(configuration), # pyright: ignore[reportArgumentType] # provider-specific embedding config is validated downstream
)
@ -105,7 +105,7 @@ class RouterVectorStoreEmbeddingExecutor:
return LiteLLMVectorStoreEmbeddingExecutor().embed(model, query, embedding_kwargs)
return self.router.embedding( # pyright: ignore[reportUnknownMemberType] # Router embedding input retains a legacy untyped list
model=model,
input=[query], # mutable-ok: Router embedding requires a mutable input list
input=[query],
**embedding_kwargs, # pyright: ignore[reportArgumentType] # provider kwargs are intentionally dynamic
)
@ -115,7 +115,7 @@ class RouterVectorStoreEmbeddingExecutor:
return await LiteLLMVectorStoreEmbeddingExecutor().aembed(model, query, embedding_kwargs)
return await self.router.aembedding( # pyright: ignore[reportUnknownMemberType] # Router embedding input retains a legacy untyped list
model=model,
input=[query], # mutable-ok: Router embedding requires a mutable input list
input=[query],
**embedding_kwargs, # pyright: ignore[reportArgumentType] # provider kwargs are intentionally dynamic
)
@ -429,4 +429,4 @@ class BaseDirectVectorStoreConfig(BaseVectorStoreConfig):
return BaseVectorStoreAuthCredentials()
def get_vector_store_endpoints_by_type(self) -> VectorStoreIndexEndpoints:
return VectorStoreIndexEndpoints(read=[], write=[]) # mutable-ok: the TypedDict declares list fields
return VectorStoreIndexEndpoints(read=[], write=[])

View file

@ -1461,7 +1461,7 @@ class BaseAWSLLM(SignsRequestsWithAWS):
@overload
def _get_boto_credentials_from_optional_params(
self,
optional_params: dict, # mutable-ok: the implementation pops the aws_* keys out of the caller's dict in place
optional_params: dict,
model: str | None = None,
bearer_token: None = None,
) -> Boto3CredentialsInfo: ...
@ -1469,7 +1469,7 @@ class BaseAWSLLM(SignsRequestsWithAWS):
@overload
def _get_boto_credentials_from_optional_params(
self,
optional_params: dict, # mutable-ok: the implementation pops the aws_* keys out of the caller's dict in place
optional_params: dict,
model: str | None = None,
*,
bearer_token: str,

View file

@ -407,7 +407,7 @@ class BedrockConverseLLM(BaseAWSLLM):
# resolved so both paths sign as the same principal. Bearer-token auth
# resolves no SigV4 principal at all, and each path reads that token
# itself.
rust_optional_params: Final = { # mutable-ok: json.dumps in the bridge rejects a mappingproxy
rust_optional_params: Final = {
**optional_params,
**_sigv4_principal(credentials),
"aws_region_name": aws_region_name,
@ -421,8 +421,8 @@ class BedrockConverseLLM(BaseAWSLLM):
stream=stream,
)
if serves_via_rust:
rust_logging_args: Final = { # mutable-ok: logging callbacks read additional_args as a plain dict
"complete_input_dict": { # mutable-ok: same, and it is serialized alongside its parent
rust_logging_args: Final = {
"complete_input_dict": {
"messages": messages,
**optional_params,
},

View file

@ -22,7 +22,7 @@ class BedrockClaudePlatformMixin(BaseAWSLLM):
self,
error_message: str,
status_code: int,
headers: dict[str, object] | httpx.Headers, # mutable-ok: base passes response headers as a dict
headers: dict[str, object] | httpx.Headers,
) -> BedrockError:
return BedrockError(status_code=status_code, message=error_message, headers=headers)

View file

@ -109,7 +109,7 @@ def merge_bedrock_aws_request_params(
server. Requests may still provide AWS credentials when the deployment has
no static credentials configured.
"""
request_params: Final = {**optional_params, **litellm_params} # mutable-ok: AWS helpers require a plain dict
request_params: Final = {**optional_params, **litellm_params}
has_static_deployment_credentials: Final = all(
isinstance(litellm_params.get(key), str) and bool(litellm_params.get(key))
for key in ("aws_access_key_id", "aws_secret_access_key", "aws_region_name")
@ -231,7 +231,7 @@ def _bedrock_model_supports(model: str, key: str) -> bool:
def apply_bedrock_invoke_structured_output(
model: str,
request_body: dict[str, object], # mutable-ok: edited in place like siblings
request_body: dict[str, object],
) -> None:
"""
Route Anthropic structured-output params to what the Bedrock model supports.
@ -256,7 +256,7 @@ def apply_bedrock_invoke_structured_output(
if isinstance(existing_output_config, dict):
existing_output_config["format"] = schema_format
else:
request_body["output_config"] = {"format": schema_format} # rebind-ok: out-param # mutable-ok: json
request_body["output_config"] = {"format": schema_format} # rebind-ok: out-param
return
verbose_logger.warning(
@ -273,7 +273,7 @@ def apply_bedrock_invoke_structured_output(
def strip_unsupported_bedrock_invoke_output_config_keys(
model: str,
request_body: dict[str, object], # mutable-ok: edited in place like siblings
request_body: dict[str, object],
) -> None:
"""
Drop ``output_config`` keys the Bedrock model does not accept.
@ -309,7 +309,7 @@ def strip_unsupported_bedrock_invoke_output_config_keys(
if preserved_format is None:
request_body.pop("output_config", None)
else:
request_body["output_config"] = {"format": preserved_format} # rebind-ok: out-param # mutable-ok: json
request_body["output_config"] = {"format": preserved_format} # rebind-ok: out-param
def normalize_custom_field_on_tools(request_body: dict) -> None:

View file

@ -59,14 +59,14 @@ class BedrockEmbedding(BaseAWSLLM):
@overload
def _load_credentials(
self,
optional_params: dict, # mutable-ok: the implementation pops the aws_* keys out of the caller's dict in place
optional_params: dict,
bearer_token: None = None,
) -> tuple[Credentials, str]: ...
@overload
def _load_credentials(
self,
optional_params: dict, # mutable-ok: the implementation pops the aws_* keys out of the caller's dict in place
optional_params: dict,
bearer_token: str,
) -> tuple[None, str]: ...

View file

@ -1390,9 +1390,7 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig):
url: Final = f"{target.endpoint_url}/{bucket_name}/"
listing_query: Final = _listing_query(configured_prefix, purpose)
continuation_query: Final = (("continuation-token", continuation_token),) if continuation_token else ()
query: Final[dict[str, str]] = dict( # mutable-ok: the base files contract returns the query as a dict
listing_query + continuation_query
)
query: Final[dict[str, str]] = dict(listing_query + continuation_query)
signed_headers: Final = self._sign_s3_request_without_body(
method="GET",
api_base=f"{url}?{urlencode(query, quote_via=quote, safe='')}",
@ -1426,7 +1424,7 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig):
_listed_managed_file(entry, bucket_name, configured_bucket_name, allow_legacy_cloud_file_ids)
for entry in listing.iterfind("{*}Contents")
)
return [ # mutable-ok: the base files contract returns a list
return [
listed_file
for listed_file in listed_files
if listed_file is not None and (purpose is None or listed_file.purpose == purpose)
@ -1471,7 +1469,7 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig):
request_params=target.request_params,
)
litellm_params[S3_SIGNED_REQUEST_HEADERS_PARAM] = signed_headers # rebind-ok: handed to validate_environment
return url, {} # mutable-ok: the base files contract returns the query as a dict
return url, {}
def _s3_request_target(
self,
@ -1488,7 +1486,7 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig):
)
region_preference: Final = request_params.s3_region_name or request_params.aws_region_name
aws_region_name: Final = self._get_aws_region_name(
optional_params={"aws_region_name": region_preference}, # mutable-ok: BaseAWSLLM takes a dict
optional_params={"aws_region_name": region_preference},
model="",
)
endpoint_url: Final = (
@ -1534,7 +1532,7 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig):
aws_request: Final = AWSRequest( # any-ok: botocore AWSRequest is untyped
method=method,
url=api_base,
headers={"x-amz-content-sha256": empty_body_hash}, # mutable-ok: botocore AWSRequest takes a dict
headers={"x-amz-content-sha256": empty_body_hash},
)
auth: Final = S3SigV4Auth(credentials, "s3", aws_region_name) # any-ok: botocore untyped
auth.add_auth(aws_request) # any-ok: botocore request mutation is untyped

View file

@ -233,7 +233,7 @@ class BedrockAmazonNovaCanvasImageEditConfig(BaseImageEditConfig):
self,
error_message: str,
status_code: int,
headers: dict[str, object] | httpx.Headers, # mutable-ok: base passes response headers as a dict
headers: dict[str, object] | httpx.Headers,
) -> BedrockError:
return BedrockError(status_code=status_code, message=error_message, headers=headers)

View file

@ -89,7 +89,7 @@ class BedrockStabilityImageEditConfig(BaseImageEditConfig):
self,
error_message: str,
status_code: int,
headers: dict[str, object] | httpx.Headers, # mutable-ok: base passes response headers as a dict
headers: dict[str, object] | httpx.Headers,
) -> BedrockError:
return BedrockError(status_code=status_code, message=error_message, headers=headers)

View file

@ -84,7 +84,7 @@ class AmazonAnthropicClaudeMessagesConfig(
self,
error_message: str,
status_code: int,
headers: dict[str, object] | httpx.Headers, # mutable-ok: base passes response headers as a dict
headers: dict[str, object] | httpx.Headers,
) -> BedrockError:
return BedrockError(status_code=status_code, message=error_message, headers=headers)

View file

@ -129,7 +129,7 @@ class BedrockPassthroughConfig(BaseAWSLLM, BedrockModelInfo, BedrockEventStreamD
self,
error_message: str,
status_code: int,
headers: dict[str, object] | httpx.Headers, # mutable-ok: base passes response headers as a dict
headers: dict[str, object] | httpx.Headers,
) -> BedrockError:
return BedrockError(status_code=status_code, message=error_message, headers=headers)

View file

@ -262,7 +262,7 @@ class BedrockRealtime(BaseAWSLLM):
if logged_events:
GLOBAL_LOGGING_WORKER.ensure_initialized_and_enqueue(
logging_obj.dispatch_success_handlers(
list(logged_events), # mutable-ok: realtime spend logging requires a list result
list(logged_events),
prefer_async_handlers=True,
)
)

View file

@ -127,7 +127,7 @@ class BedrockRealtimeConfig(BaseRealtimeConfig):
self,
error_message: str,
status_code: int,
headers: dict[str, object] | httpx.Headers, # mutable-ok: base passes response headers as a dict
headers: dict[str, object] | httpx.Headers,
) -> BedrockError:
return BedrockError(status_code=status_code, message=error_message, headers=headers)
@ -887,7 +887,7 @@ class BedrockRealtimeConfig(BaseRealtimeConfig):
id=f"resp_{uuid.uuid4()}",
status="completed",
conversation_id=f"conv_{uuid.uuid4()}",
usage=dict(usage), # mutable-ok: OpenAIRealtimeResponseDoneObject types usage as plain dict
usage=dict(usage),
),
)
return (leftover_done,)

View file

@ -165,11 +165,11 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM):
def validate_environment(
self,
headers: dict, # mutable-ok: BaseSearchConfig hands providers the mutable request header dict
headers: dict,
api_key: str | None = None,
api_base: str | None = None,
**kwargs: object, # kwargs-ok: BaseSearchConfig.validate_environment forwards provider-specific extras
) -> dict: # mutable-ok: the handler passes these headers straight to httpx, which wants a dict
) -> dict:
"""
Set MCP transport headers. Per the MCP Streamable HTTP transport spec,
the client MUST accept both application/json and text/event-stream, and
@ -178,7 +178,7 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM):
Authentication itself happens in sign_request(): bearer token for
CUSTOM_JWT gateways, AWS SigV4 for AWS_IAM gateways.
"""
return { # mutable-ok: httpx request headers are a dict
return {
**headers,
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
@ -189,7 +189,7 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM):
def get_complete_url(
self,
api_base: str | None,
optional_params: dict, # mutable-ok: BaseSearchConfig passes optional params as a dict
optional_params: dict,
data: dict | list[dict] | None = None, # mutable-ok: BaseSearchConfig request bodies are JSON dicts
**kwargs: object, # kwargs-ok: BaseSearchConfig.get_complete_url forwards provider-specific extras
) -> str:
@ -205,9 +205,9 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM):
def transform_search_request(
self,
query: str | list[str], # mutable-ok: BaseSearchConfig accepts a list of queries
optional_params: dict, # mutable-ok: BaseSearchConfig passes optional params as a dict
optional_params: dict,
**kwargs: object, # kwargs-ok: BaseSearchConfig.transform_search_request forwards provider-specific extras
) -> dict: # mutable-ok: the JSON-RPC body is serialized as a JSON object
) -> dict:
"""
Transform Search request to an MCP tools/call request.
@ -234,13 +234,13 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM):
"Other gateway tools cannot be invoked through this provider."
)
return { # mutable-ok: JSON-RPC request bodies are JSON objects
return {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { # mutable-ok: JSON-RPC request bodies are JSON objects
"params": {
"name": tool_name,
"arguments": { # mutable-ok: JSON-RPC request bodies are JSON objects
"arguments": {
"query": joined_query[:AGENTCORE_MAX_QUERY_LENGTH],
"maxResults": optional_params.get("max_results", AGENTCORE_DEFAULT_MAX_RESULTS),
},
@ -249,12 +249,12 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM):
def sign_request(
self,
headers: dict[str, str], # mutable-ok: BaseSearchConfig hands providers the mutable request header dict
optional_params: dict[str, object], # mutable-ok: BaseSearchConfig passes optional params as a dict
headers: dict[str, str],
optional_params: dict[str, object],
request_data: dict[str, object] | list[dict[str, object]], # mutable-ok: request bodies are JSON dicts
api_base: str,
api_key: str | None = None,
) -> tuple[dict[str, str], bytes | None]: # mutable-ok: BaseSearchConfig.sign_request returns httpx headers
) -> tuple[dict[str, str], bytes | None]:
"""
Authenticate the MCP request.
@ -286,7 +286,7 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM):
default_api_base=api_base if gateway_host_match else None,
)
if bearer_token:
bearer_headers: Final = { # mutable-ok: httpx request headers are a dict
bearer_headers: Final = {
**headers,
"Authorization": f"Bearer {bearer_token}",
}
@ -302,7 +302,7 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM):
signing_params: Final = (
optional_params
if optional_params.get("aws_region_name") is not None
else { # mutable-ok: BaseAWSLLM._sign_request takes optional params as a dict
else {
**optional_params,
"aws_region_name": self._signing_region(api_base),
}
@ -398,7 +398,7 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM):
structured: Final = result.get("structuredContent") if isinstance(result, Mapping) else None
items: Final = text_items or _result_items(structured)
results: Final = [_to_search_result(item) for item in items] # mutable-ok: pydantic list field
results: Final = [_to_search_result(item) for item in items]
return SearchResponse(results=results, object="search")
@ -448,7 +448,7 @@ class AgentCoreSearchConfig(BaseSearchConfig, BaseAWSLLM):
self,
error_message: str,
status_code: int,
headers: dict, # mutable-ok: BaseSearchConfig.get_error_class takes the response headers as a dict
headers: dict,
) -> Exception:
return BedrockError(
status_code=status_code,

View file

@ -43,7 +43,7 @@ class BedrockVectorStoreConfig(BaseVectorStoreConfig, BaseAWSLLM):
self,
error_message: str,
status_code: int,
headers: dict[str, object] | httpx.Headers, # mutable-ok: base passes response headers as a dict
headers: dict[str, object] | httpx.Headers,
) -> BedrockError:
return BedrockError(status_code=status_code, message=error_message, headers=headers)

View file

@ -55,7 +55,7 @@ class BedrockMantlePassthroughConfig(BedrockPassthroughConfig):
model: str,
custom_llm_provider: str,
httpx_response: Response,
request_data: dict, # mutable-ok: mirrors the inherited BedrockPassthroughConfig signature
request_data: dict,
logging_obj: Logging,
endpoint: str,
) -> Optional["CostResponseTypes"]:

View file

@ -214,15 +214,11 @@ class BedrockMantleResponsesAPIConfig(BedrockMantleAuthMixin, OpenAIResponsesAPI
summary,
sorted(_BEDROCK_MANTLE_OPENAI_PATH_SUPPORTED_REASONING_SUMMARIES),
)
stripped: Final = { # mutable-ok: map_openai_params contract returns a plain dict
key: value for key, value in reasoning.items() if key != "summary"
}
stripped: Final = {key: value for key, value in reasoning.items() if key != "summary"}
return (
{**params, "reasoning": stripped} # mutable-ok: map_openai_params contract returns a plain dict
{**params, "reasoning": stripped}
if stripped
else { # mutable-ok: map_openai_params contract returns a plain dict
key: value for key, value in params.items() if key != "reasoning"
}
else {key: value for key, value in params.items() if key != "reasoning"}
)
def transform_responses_api_request(
@ -372,7 +368,7 @@ class BedrockMantleResponsesAPIConfig(BedrockMantleAuthMixin, OpenAIResponsesAPI
"Bedrock Mantle Responses API: rewrote Codex input item type(s) %s that Mantle rejects.",
rewritten_types,
)
kept: Final = [item for item, _ in normalized if item is not None] # mutable-ok: ResponseInputParam is a list
kept: Final = [item for item, _ in normalized if item is not None]
return kept # pyright: ignore[reportReturnType] # Codex passthrough items sit outside the OpenAI input union
def map_openai_params(

View file

@ -139,13 +139,13 @@ class CohereParseConfig(BaseOCRConfig):
"""Cohere Parse, an image-only document understanding endpoint returning markdown or blocks."""
def get_supported_ocr_params(self, model: str) -> list[str]: # mutable-ok: BaseOCRConfig signature
return list(COHERE_PARSE_SUPPORTED_PARAMS) # mutable-ok: BaseOCRConfig signature
return list(COHERE_PARSE_SUPPORTED_PARAMS)
def get_api_key_env_var(self) -> str | None:
return COHERE_API_KEY_ENV_VAR
def get_health_check_document(self) -> DocumentType:
return { # mutable-ok: litellm.aocr rejects any document that is not a dict
return {
"type": "image_url",
"image_url": COHERE_PARSE_HEALTH_CHECK_IMAGE_DATA_URI,
}
@ -158,7 +158,7 @@ class CohereParseConfig(BaseOCRConfig):
non_default_params: Mapping[str, object],
optional_params: Mapping[str, object],
model: str,
) -> dict[str, object]: # mutable-ok: BaseOCRConfig signature
) -> dict[str, object]:
output_format: Final = non_default_params.get(COHERE_PARSE_OUTPUT_FORMAT_PARAM)
if output_format is not None and output_format not in COHERE_PARSE_OUTPUT_FORMATS:
raise UnsupportedParamsError(
@ -179,7 +179,7 @@ class CohereParseConfig(BaseOCRConfig):
)
if value is not None
)
return {**optional_params, **dict(overrides)} # mutable-ok: BaseOCRConfig signature
return {**optional_params, **dict(overrides)}
def validate_environment(
self,
@ -189,14 +189,14 @@ class CohereParseConfig(BaseOCRConfig):
api_base: str | None = None,
litellm_params: Mapping[str, object] | None = None,
**kwargs: object, # kwargs-ok: BaseOCRConfig.validate_environment signature
) -> dict[str, str]: # mutable-ok: BaseOCRConfig signature
) -> dict[str, str]:
resolved_key: Final = api_key or get_secret_str(COHERE_API_KEY_ENV_VAR)
if resolved_key is None:
raise ValueError(
f"Missing {COHERE_API_KEY_ENV_VAR} - set it in the environment or pass api_key to "
"litellm.ocr()/litellm.aocr()"
)
return { # mutable-ok: BaseOCRConfig signature
return {
"Authorization": f"Bearer {resolved_key}",
"Content-Type": "application/json",
**headers,
@ -242,7 +242,7 @@ class CohereParseConfig(BaseOCRConfig):
optional_params.get(COHERE_PARSE_OUTPUT_FORMAT_PARAM, COHERE_PARSE_DEFAULT_OUTPUT_FORMAT)
),
}
return OCRRequestData(data=dict(body), files=None) # mutable-ok: OCRRequestData.data is a dict
return OCRRequestData(data=dict(body), files=None)
def transform_ocr_request(
self,
@ -276,9 +276,7 @@ class CohereParseConfig(BaseOCRConfig):
) -> OCRResponse:
native: Final = _NATIVE_RESPONSE_ADAPTER.validate_python(raw_response.json())
parsed: Final = _CohereParseResponse.model_validate(native)
pages: Final = [ # mutable-ok: OCRResponse.pages is a list
_normalize_page(page, position) for position, page in enumerate(parsed.pages)
]
pages: Final = [_normalize_page(page, position) for position, page in enumerate(parsed.pages)]
billed_pages: Final = _billed_pages(parsed)
response: Final = OCRResponse(
pages=pages,

View file

@ -544,8 +544,8 @@ class BaseLLMHTTPHandler:
)
def sign_and_log(
transformed: dict[str, object], # mutable-ok: async_completion takes dict
) -> tuple[dict[str, object], dict[str, object], bytes | None]: # mutable-ok: async_completion takes dict
transformed: dict[str, object],
) -> tuple[dict[str, object], dict[str, object], bytes | None]:
data: Final = {**transformed, **extra_body} if extra_body is not None else transformed
signed: Final = cast( # cast-ok: sign_request is declared as a bare dict
"tuple[dict[str, object], bytes | None]",
@ -577,8 +577,8 @@ class BaseLLMHTTPHandler:
return data, signed[0], signed[1]
def dispatch_async(
data: dict[str, object], # mutable-ok: async_completion takes dict
signed_headers: dict[str, object], # mutable-ok: async_completion takes dict
data: dict[str, object],
signed_headers: dict[str, object],
signed_json_body: bytes | None,
) -> Coroutine[object, object, ModelResponse | CustomStreamWrapper]:
async_client: Final = client if isinstance(client, AsyncHTTPHandler) else None
@ -2817,7 +2817,7 @@ class BaseLLMHTTPHandler:
)
if self._has_agentic_completion_hook(logging_obj):
agentic_kwargs: Final = dict(litellm_params) # mutable-ok: agentic hooks mutate kwargs in place
agentic_kwargs: Final = dict(litellm_params)
final_response: Final = run_async_function(
self._call_agentic_completion_hooks,
response=initial_response,
@ -3008,7 +3008,7 @@ class BaseLLMHTTPHandler:
logging_obj=logging_obj,
)
agentic_kwargs: Final = dict(litellm_params) # mutable-ok: agentic hooks mutate kwargs in place
agentic_kwargs: Final = dict(litellm_params)
final_response: Final = await self._call_agentic_completion_hooks(
response=initial_response,
model=model,
@ -4988,9 +4988,7 @@ class BaseLLMHTTPHandler:
files_per_page: Final = self._files_per_listing_page(
response, provider_config, logging_obj, litellm_params, headers, sync_httpx_client, timeout
)
return [ # mutable-ok: the files contract returns the listing as a list
listed_file for page_files in files_per_page for listed_file in page_files
]
return [listed_file for page_files in files_per_page for listed_file in page_files]
async def async_list_files(
self,
@ -5045,17 +5043,15 @@ class BaseLLMHTTPHandler:
files_per_page: Final = self._files_per_async_listing_page(
response, provider_config, logging_obj, litellm_params, headers, async_httpx_client, timeout
)
return [ # mutable-ok: the files contract returns the listing as a list
listed_file async for page_files in files_per_page for listed_file in page_files
]
return [listed_file async for page_files in files_per_page for listed_file in page_files]
def _files_per_listing_page(
self,
first_page: httpx.Response,
provider_config: BaseFilesConfig,
logging_obj: LiteLLMLoggingObj,
litellm_params: dict, # mutable-ok: handed to the files contract, which types it as a dict
headers: dict, # mutable-ok: handed to validate_environment, which types it as a dict
litellm_params: dict,
headers: dict,
client: HTTPHandler,
timeout: float | httpx.Timeout | None,
) -> Iterator[list[OpenAIFileObject]]: # mutable-ok: each page arrives as the list the files contract returns
@ -5082,8 +5078,8 @@ class BaseLLMHTTPHandler:
first_page: httpx.Response,
provider_config: BaseFilesConfig,
logging_obj: LiteLLMLoggingObj,
litellm_params: dict, # mutable-ok: handed to the files contract, which types it as a dict
headers: dict, # mutable-ok: handed to validate_environment, which types it as a dict
litellm_params: dict,
headers: dict,
client: AsyncHTTPHandler,
timeout: float | httpx.Timeout | None,
) -> AsyncIterator[list[OpenAIFileObject]]: # mutable-ok: each page arrives as the list the files contract returns
@ -5108,9 +5104,9 @@ class BaseLLMHTTPHandler:
def _next_listing_page_headers(
self,
provider_config: BaseFilesConfig,
headers: dict, # mutable-ok: handed to validate_environment, which types it as a dict
litellm_params: dict, # mutable-ok: handed to the files contract, which types it as a dict
) -> dict: # mutable-ok: validate_environment returns the header dict the files contract types
headers: dict,
litellm_params: dict,
) -> dict:
return provider_config.validate_environment(
api_key=litellm_params.get("api_key"),
headers=headers,
@ -5124,9 +5120,9 @@ class BaseLLMHTTPHandler:
self,
latest_page: httpx.Response,
provider_config: BaseFilesConfig,
litellm_params: dict, # mutable-ok: handed to the files contract, which types it as a dict
litellm_params: dict,
listed_count: int,
) -> tuple[str, dict[str, str]] | None: # mutable-ok: the base files contract returns the query as a dict
) -> tuple[str, dict[str, str]] | None:
if listed_count >= MAX_FILE_LIST_LIMIT:
return None
return provider_config.transform_list_files_next_request(
@ -9863,7 +9859,7 @@ class BaseLLMHTTPHandler:
logging_obj.pre_call(
input="",
api_key="",
additional_args={ # mutable-ok: pre_call's additional_args contract is a dict
additional_args={
"query": query,
"vector_store_id": vector_store_id,
"api_base": endpoint,
@ -9899,7 +9895,7 @@ class BaseLLMHTTPHandler:
query=query,
vector_store_search_optional_params=vector_store_search_optional_params,
litellm_logging_obj=logging_obj,
litellm_params=dict(litellm_params), # mutable-ok: snapshot GenericLiteLLMParams into the Mapping shape
litellm_params=dict(litellm_params),
embedding_executor=embedding_executor,
timeout=timeout,
)
@ -10039,7 +10035,7 @@ class BaseLLMHTTPHandler:
query=query,
vector_store_search_optional_params=vector_store_search_optional_params,
litellm_logging_obj=logging_obj,
litellm_params=dict(litellm_params), # mutable-ok: snapshot GenericLiteLLMParams into the Mapping shape
litellm_params=dict(litellm_params),
embedding_executor=embedding_executor,
timeout=timeout,
)

View file

@ -129,7 +129,7 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
forward_images: Final = any(
isinstance(message.get("content"), list) for message in messages
) and supports_vision(model=model, custom_llm_provider="deepseek")
transformed: Final = [ # mutable-ok: provider messages must stay JSON-array lists the base transform mutates
transformed: Final = [
self._forward_or_collapse_content(message=message, forward_images=forward_images) for message in messages
]
@ -155,7 +155,7 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
collapsed: Final = convert_content_list_to_str(message=message)
if not collapsed or collapsed == content:
return message
collapsed_message: Final = {**message, "content": collapsed} # mutable-ok: wire messages are plain JSON dicts
collapsed_message: Final = {**message, "content": collapsed}
return cast(AllMessageValues, collapsed_message) # cast-ok: TypedDict spread narrows to dict
def _is_vision_forwardable_content(self, message: AllMessageValues, content: Sequence[object]) -> bool:
@ -204,8 +204,8 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
search_text: Final = extract_search_results_text(message_fields.get("search_results"))
if not search_text:
return message
forwarded_content: Final = [*content, {"type": "text", "text": search_text}] # mutable-ok: JSON-array content
forwarded: Final = { # mutable-ok: wire messages are plain JSON dicts
forwarded_content: Final = [*content, {"type": "text", "text": search_text}]
forwarded: Final = {
**{key: value for key, value in message_fields.items() if key != "search_results"},
"content": forwarded_content,
}

View file

@ -60,12 +60,10 @@ class FalAIGPTImage2Config(FalAIBaseConfig):
endpoint: Final[str] = model if model.startswith(self.MODEL_PREFIX) else f"{self.MODEL_PREFIX}{model}"
return f"{base_url}/{endpoint}"
def get_supported_openai_params( # mutable-ok: base class contract returns a list
self, model: str
) -> list[OpenAIImageGenerationOptionalParams]:
return list(SUPPORTED_OPENAI_PARAMS) # mutable-ok: base class contract returns a list
def get_supported_openai_params(self, model: str) -> list[OpenAIImageGenerationOptionalParams]:
return list(SUPPORTED_OPENAI_PARAMS)
def map_openai_params( # mutable-ok: base class contract returns a dict
def map_openai_params(
self,
non_default_params: Mapping[str, object],
optional_params: Mapping[str, object],
@ -88,7 +86,7 @@ class FalAIGPTImage2Config(FalAIBaseConfig):
if key in self.PARAM_TRANSLATION and self.PARAM_TRANSLATION[key] not in optional_params
}
)
return {**optional_params, **translated_params} # mutable-ok: base class contract returns a dict
return {**optional_params, **translated_params}
def _translate_value(self, key: str, value: object) -> object:
if key == "size":
@ -113,7 +111,7 @@ class FalAIGPTImage2Config(FalAIBaseConfig):
normalized: Final[str] = self.OPENAI_QUALITY_ALIASES.get(quality, quality)
return normalized if normalized in self.SUPPORTED_QUALITIES else "auto"
def transform_image_generation_request( # mutable-ok: base class contract returns a dict
def transform_image_generation_request(
self,
model: str,
prompt: str,
@ -121,4 +119,4 @@ class FalAIGPTImage2Config(FalAIBaseConfig):
litellm_params: Mapping[str, object],
headers: Mapping[str, str],
) -> dict:
return {"prompt": prompt, **optional_params} # mutable-ok: base class contract returns a dict
return {"prompt": prompt, **optional_params}

View file

@ -69,7 +69,7 @@ def _extract_fireworks_hidden_params(payload: dict) -> dict:
def _json_schema_response_format(schema: object, name: str) -> Mapping[str, object]:
return {"type": "json_schema", "json_schema": {"name": name, "schema": schema}} # mutable-ok: JSON request body
return {"type": "json_schema", "json_schema": {"name": name, "schema": schema}}
EFFORT_KWARG_KEYS: Final = frozenset({"enable_thinking", "thinking", "reasoning_budget", "low_effort"})
@ -339,12 +339,10 @@ class FireworksAIConfig(FireworksAIMixin, OpenAIGPTConfig):
return optional_params
def map_extra_body_params(
self, optional_params: Mapping[str, object], model: str
) -> dict: # mutable-ok: http handler pops extra_body off the returned dict
def map_extra_body_params(self, optional_params: Mapping[str, object], model: str) -> dict:
extra_body: Final = optional_params.get("extra_body")
if not isinstance(extra_body, dict):
return dict(optional_params) # mutable-ok: JSON request body
return dict(optional_params)
stripped: Final = tuple(sorted(k for k in extra_body if k in NIM_VLLM_STRIP_PARAMS))
if stripped:
@ -368,11 +366,11 @@ class FireworksAIConfig(FireworksAIMixin, OpenAIGPTConfig):
if k not in _EXTRA_BODY_CONSUMED_PARAMS
and (k != "response_format" or "response_format" not in optional_params)
)
base: Final = {k: v for k, v in optional_params.items() if k != "extra_body"} # mutable-ok: JSON request body
return { # mutable-ok: JSON request body
base: Final = {k: v for k, v in optional_params.items() if k != "extra_body"}
return {
**base,
**dict(promoted), # mutable-ok: JSON request body
**({"extra_body": dict(remaining)} if remaining else {}), # mutable-ok: JSON request body
**dict(promoted),
**({"extra_body": dict(remaining)} if remaining else {}),
}
@staticmethod
@ -441,12 +439,12 @@ class FireworksAIConfig(FireworksAIMixin, OpenAIGPTConfig):
if extra_body.get("guided_json") is not None:
return (("response_format", _json_schema_response_format(extra_body["guided_json"], "response")),)
if extra_body.get("guided_grammar") is not None:
grammar_response_format: Final = { # mutable-ok: JSON request body
grammar_response_format: Final = {
"type": "grammar",
"grammar": extra_body["guided_grammar"],
}
return (("response_format", grammar_response_format),)
choice_schema: Final = { # mutable-ok: JSON request body
choice_schema: Final = {
"type": "string",
"enum": extra_body["guided_choice"],
}

View file

@ -109,4 +109,4 @@ class FireworksAIMixin:
def _add_session_affinity_header(self, headers: dict, litellm_params: dict) -> dict:
pinned: Final = with_fireworks_session_affinity(headers, litellm_params)
return dict(pinned) # mutable-ok: the HTTP handler updates the returned headers in place
return dict(pinned)

View file

@ -54,18 +54,14 @@ class FireworksAITextCompletionConfig(FireworksAIMixin, BaseTextCompletionConfig
optional_params[k] = v
return optional_params
def map_extra_body_params(
self, optional_params: Mapping[str, object], model: str
) -> dict: # mutable-ok: returned dict is spread into the OpenAI SDK call as kwargs
def map_extra_body_params(self, optional_params: Mapping[str, object], model: str) -> dict:
raw_extra_body: Final = optional_params.get("extra_body")
initial_body: Final = (
dict(raw_extra_body) if isinstance(raw_extra_body, dict) else {} # mutable-ok: JSON request body
)
initial_body: Final = dict(raw_extra_body) if isinstance(raw_extra_body, dict) else {}
stripped_body: Final = self._strip_unsupported_params(initial_body, model)
moved_body: Final = self._move_native_params_into_extra_body(stripped_body, optional_params)
effort_body: Final = self._translate_chat_template_kwargs(moved_body, optional_params, model)
final_body: Final = self._translate_guided_into_extra_body(effort_body, optional_params)
base: Final = { # mutable-ok: JSON request body
base: Final = {
k: v
for k, v in optional_params.items()
if k not in ("extra_body", "response_format", "reasoning_effort", "thinking")
@ -75,9 +71,7 @@ class FireworksAITextCompletionConfig(FireworksAIMixin, BaseTextCompletionConfig
return base
@staticmethod
def _strip_unsupported_params(
extra_body: Mapping[str, object], model: str
) -> dict: # mutable-ok: JSON request body
def _strip_unsupported_params(extra_body: Mapping[str, object], model: str) -> dict:
stripped: Final = tuple(sorted(k for k in extra_body if k in _TEXT_COMPLETION_STRIP_PARAMS))
if stripped:
verbose_logger.debug(
@ -85,15 +79,13 @@ class FireworksAITextCompletionConfig(FireworksAIMixin, BaseTextCompletionConfig
stripped,
model,
)
return { # mutable-ok: JSON request body
k: v for k, v in extra_body.items() if k not in _TEXT_COMPLETION_STRIP_PARAMS
}
return {k: v for k, v in extra_body.items() if k not in _TEXT_COMPLETION_STRIP_PARAMS}
@staticmethod
def _move_native_params_into_extra_body(
extra_body: Mapping[str, object], optional_params: Mapping[str, object]
) -> dict: # mutable-ok: JSON request body
moved: Final = dict(extra_body) # mutable-ok: JSON request body
) -> dict:
moved: Final = dict(extra_body)
for key in ("response_format", "reasoning_effort", "thinking"):
value = optional_params.get(key)
if value is None:
@ -105,13 +97,11 @@ class FireworksAITextCompletionConfig(FireworksAIMixin, BaseTextCompletionConfig
def _translate_chat_template_kwargs(
self, extra_body: Mapping[str, object], optional_params: Mapping[str, object], model: str
) -> dict: # mutable-ok: JSON request body
) -> dict:
chat_template_kwargs: Final = extra_body.get("chat_template_kwargs")
if chat_template_kwargs is None:
return dict(extra_body) # mutable-ok: JSON request body
result: Final = { # mutable-ok: JSON request body
k: v for k, v in extra_body.items() if k != "chat_template_kwargs"
}
return dict(extra_body)
result: Final = {k: v for k, v in extra_body.items() if k != "chat_template_kwargs"}
if not isinstance(chat_template_kwargs, dict):
verbose_logger.debug(
"fireworks_ai dropping chat_template_kwargs for model=%s; expected an object, got %s.",
@ -140,18 +130,18 @@ class FireworksAITextCompletionConfig(FireworksAIMixin, BaseTextCompletionConfig
model,
)
return result
return {**result, "reasoning_effort": effort} # mutable-ok: JSON request body
return {**result, "reasoning_effort": effort}
@staticmethod
def _translate_guided_into_extra_body(
extra_body: Mapping[str, object], optional_params: Mapping[str, object]
) -> dict: # mutable-ok: JSON request body
) -> dict:
guided_response_format: Final = FireworksAIConfig.translate_guided_params(extra_body, optional_params)
remaining: Final = { # mutable-ok: JSON request body
remaining: Final = {
k: v for k, v in extra_body.items() if k not in ("guided_json", "guided_grammar", "guided_choice")
}
if guided_response_format:
return { # mutable-ok: JSON request body
return {
**remaining,
guided_response_format[0][0]: guided_response_format[0][1],
}

View file

@ -111,9 +111,7 @@ def _with_instruction_items_folded(
joined: Final = "\n\n".join(chunk for chunk in (instructions, *folded.values()) if chunk)
return (
instructions if not folded else joined or None,
[ # mutable-ok: the base class takes the input items as a list
_developer_item_as_system(item) for index, item in enumerate(items) if index not in folded
],
[_developer_item_as_system(item) for index, item in enumerate(items) if index not in folded],
)
@ -127,7 +125,7 @@ class FireworksAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
headers: Mapping[str, str],
model: str,
litellm_params: GenericLiteLLMParams | None,
) -> dict: # mutable-ok: overrides the base class signature
) -> dict:
params: Final = litellm_params or GenericLiteLLMParams()
api_key: Final = resolve_fireworks_api_key(params.api_key)
if api_key is None:
@ -136,7 +134,7 @@ class FireworksAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
{"Content-Type": "application/json", **headers, "Authorization": f"Bearer {api_key}"}
)
pinned: Final = with_fireworks_session_affinity(authorized, _session_params(params))
return dict(pinned) # mutable-ok: the HTTP handler updates the returned headers in place
return dict(pinned)
def get_complete_url(self, api_base: str | None, litellm_params: Mapping[str, object]) -> str:
base: Final = (api_base or get_secret_str("FIREWORKS_API_BASE") or FIREWORKS_AI_DEFAULT_API_BASE).rstrip("/")
@ -146,10 +144,10 @@ class FireworksAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
self,
model: str,
input: str | ResponseInputParam,
response_api_optional_request_params: dict, # mutable-ok: overrides the base class signature
response_api_optional_request_params: dict,
litellm_params: GenericLiteLLMParams,
headers: dict, # mutable-ok: overrides the base class signature
) -> dict: # mutable-ok: overrides the base class signature
headers: dict,
) -> dict:
instructions_param: Final[object] = response_api_optional_request_params.get("instructions")
validated_input: Final = self._validate_input_param(input)
instructions, folded_input = (
@ -158,7 +156,7 @@ class FireworksAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
else (instructions_param, _developer_items_as_system(validated_input))
)
instruction_entries: Final = () if instructions is None else (("instructions", instructions),)
folded_params: Final = { # mutable-ok: the base class takes the optional params as a dict
folded_params: Final = {
key: value
for key, value in (
*((key, value) for key, value in response_api_optional_request_params.items() if key != "instructions"),

View file

@ -47,7 +47,7 @@ class GeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
def get_supported_openai_params(
self, model: str
) -> list[OpenAIAudioTranscriptionOptionalParams]: # mutable-ok: BaseAudioTranscriptionConfig signature
return ["language", "response_format", "timestamp_granularities"] # mutable-ok: base contract returns a list
return ["language", "response_format", "timestamp_granularities"]
@property
def supports_subtitle_synthesis(self) -> bool:
@ -59,16 +59,16 @@ class GeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
optional_params: Mapping[str, object],
model: str,
drop_params: bool,
) -> dict: # mutable-ok: BaseAudioTranscriptionConfig signature
) -> dict:
supported_params: Final = frozenset(self.get_supported_openai_params(model))
accepted: Final = tuple((k, v) for k, v in non_default_params.items() if k in supported_params)
return dict((*optional_params.items(), *accepted)) # mutable-ok: base contract returns a plain dict
return dict((*optional_params.items(), *accepted))
def get_error_class(
self,
error_message: str,
status_code: int,
headers: dict | Headers, # mutable-ok: base signature and BaseLLMException take dict | Headers
headers: dict | Headers,
) -> BaseLLMException:
return GeminiError(status_code=status_code, message=error_message, headers=headers)
@ -81,14 +81,14 @@ class GeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
litellm_params: Mapping[str, object],
api_key: str | None = None,
api_base: str | None = None,
) -> dict: # mutable-ok: BaseAudioTranscriptionConfig signature
) -> dict:
resolved_api_key: Final = GeminiModelInfo.get_api_key(api_key)
if not resolved_api_key:
raise GeminiError(
status_code=401,
message="Google API key is required. Set GOOGLE_API_KEY or GEMINI_API_KEY environment variable.",
)
return { # mutable-ok: the http handler passes these headers straight to httpx
return {
**headers,
"Content-Type": "application/json",
"x-goog-api-key": resolved_api_key,
@ -125,7 +125,7 @@ class GeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
audio_input=audio_input,
transcription_config=_build_transcription_config(optional_params),
)
return AudioTranscriptionRequestData(data=dict(request)) # mutable-ok: AudioTranscriptionRequestData wants dict
return AudioTranscriptionRequestData(data=dict(request))
def transform_audio_transcription_response(
self,
@ -159,7 +159,7 @@ class GeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
if (word := _annotation_to_word(annotation)) is not None
)
if words:
response["words"] = list(words) # mutable-ok: verbose_json words is a JSON array
response["words"] = list(words)
last_word_end: Final = words[-1].get("end")
if last_word_end is not None:
response["duration"] = last_word_end
@ -244,7 +244,7 @@ def _annotation_to_word(annotation: GeminiTranscriptionWordAnnotation) -> Mappin
("end", _parse_offset_seconds(annotation.end_offset)),
("speaker", annotation.speaker),
)
return {key: value for key, value in entries if value is not None} # mutable-ok: word entries serialize to JSON
return {key: value for key, value in entries if value is not None}
def _parse_offset_seconds(offset: str | None) -> float | None:

View file

@ -7,7 +7,7 @@ from litellm.llms.gemini.google_genai.guardrail_translation.handler import (
)
from litellm.types.utils import CallTypes
guardrail_translation_mappings: Final = { # mutable-ok: discover_guardrail_translation_mappings only accepts isinstance(mappings, dict)
guardrail_translation_mappings: Final = {
CallTypes.generate_content: GoogleGenAIGenerateContentHandler,
CallTypes.agenerate_content: GoogleGenAIGenerateContentHandler,
CallTypes.generate_content_stream: GoogleGenAIGenerateContentHandler,

View file

@ -96,7 +96,7 @@ def _part_texts(text_parts: Sequence[object]) -> tuple[str, ...]:
def _texts_payload(
texts: Sequence[str],
) -> list[str]: # mutable-ok: GenericGuardrailAPIInputs.texts is declared list[str]
return list(texts) # mutable-ok: GenericGuardrailAPIInputs.texts is declared list[str]
return list(texts)
def _write_back_texts(text_parts: Sequence[object], guardrailed_texts: Sequence[str] | None) -> None:
@ -157,7 +157,7 @@ class GoogleGenAIGenerateContentHandler(BaseTranslation):
async def process_input_messages(
self,
data: dict, # mutable-ok: base handler contract passes the proxy's request dict through to apply_guardrail
data: dict,
guardrail_to_apply: "CustomGuardrail",
litellm_logging_obj: Optional["LiteLLMLoggingObj"] = None,
) -> object:
@ -245,11 +245,11 @@ class GoogleGenAIGenerateContentHandler(BaseTranslation):
user_api_key_dict: Optional["UserAPIKeyAuth"],
context_key: str,
context_value: object,
) -> dict: # mutable-ok: CustomGuardrail.apply_guardrail requires a plain dict request payload
) -> dict:
base: Final = request_data if request_data is not None else _EMPTY_REQUEST_DATA
user_metadata: Final = self.transform_user_api_key_dict_to_metadata(user_api_key_dict)
context_pairs: Final = ((context_key, context_value),) if context_key not in base else ()
metadata_pairs: Final = (
(("litellm_metadata", user_metadata),) if user_metadata and "litellm_metadata" not in base else ()
)
return dict((*base.items(), *context_pairs, *metadata_pairs)) # mutable-ok: apply_guardrail takes a plain dict
return dict((*base.items(), *context_pairs, *metadata_pairs))

View file

@ -30,7 +30,7 @@ class GigaChatModelResponseIterator:
def chunk_parser(self, chunk: Mapping[str, object]) -> GenericStreamingChunk:
"""Parse a single streaming chunk from GigaChat."""
choices: Sequence = chunk.get("choices") or () # mutable-ok: tuple literal as default
choices: Sequence = chunk.get("choices") or ()
if not choices:
return GenericStreamingChunk(
text="",
@ -42,7 +42,7 @@ class GigaChatModelResponseIterator:
)
choice: Final = choices[0]
delta: Mapping[str, object] = choice.get("delta") or {} # mutable-ok: empty dict default for get
delta: Mapping[str, object] = choice.get("delta") or {}
chunk_finish_reason: Final = choice.get("finish_reason")
# Extract text content
@ -74,7 +74,7 @@ class GigaChatModelResponseIterator:
)
finish_reason = "tool_calls"
usage_data: Final = chunk.get("usage") or {} # mutable-ok: empty dict default
usage_data: Final = chunk.get("usage") or {}
if usage_data and isinstance(usage_data, dict):
validated_usage: Final = {k: int(v) for k, v in usage_data.items()}
usage = convert_usage(validated_usage)

View file

@ -104,14 +104,14 @@ class GigaChatConfig(BaseConfig):
def validate_environment(
self,
headers: dict, # mutable-ok: mutates in place per GigaChat OAuth setup
headers: dict,
model: str,
messages: Sequence[AllMessageValues],
optional_params: Mapping[str, object],
litellm_params: Mapping[str, object],
api_key: str | None = None,
api_base: str | None = None,
) -> dict: # mutable-ok: base class contract returns dict for httpx
) -> dict:
"""
Set up headers with OAuth token.
"""
@ -129,7 +129,7 @@ class GigaChatConfig(BaseConfig):
def get_supported_openai_params(self, model: str) -> list[str]: # mutable-ok: base class contract returns list
"""Return list of supported OpenAI parameters."""
return [ # mutable-ok: base class contract returns list
return [
"stream",
"temperature",
"top_p",
@ -146,10 +146,10 @@ class GigaChatConfig(BaseConfig):
def map_openai_params(
self,
non_default_params: Mapping[str, object],
optional_params: dict, # mutable-ok: mutated in place per GigaChat mapping
optional_params: dict,
model: str,
drop_params: bool,
) -> dict: # mutable-ok: base class contract returns dict
) -> dict:
"""Map OpenAI parameters to GigaChat parameters."""
for param, value in non_default_params.items():
if param == "stream":
@ -188,7 +188,7 @@ class GigaChatConfig(BaseConfig):
schema_name = json_schema.get("name", "structured_output")
schema = json_schema.get("schema", {})
function_def = { # mutable-ok: request payload for httpx
function_def = {
"name": schema_name,
"description": f"Output structured response: {schema_name}",
"parameters": schema,
@ -203,7 +203,7 @@ class GigaChatConfig(BaseConfig):
),
function_def,
]
optional_params["function_call"] = {"name": schema_name} # mutable-ok: request payload
optional_params["function_call"] = {"name": schema_name}
optional_params["_structured_output"] = True
return optional_params
@ -321,7 +321,7 @@ class GigaChatConfig(BaseConfig):
optional_params: Mapping[str, object],
litellm_params: Mapping[str, object],
headers: Mapping[str, object],
) -> dict: # mutable-ok: request payload sent to httpx
) -> dict:
"""Transform OpenAI request to GigaChat format."""
giga_messages: Final = self._transform_messages(messages)

View file

@ -112,7 +112,7 @@ class GigaChatEmbeddingConfig(BaseEmbeddingConfig):
"input": ["text1", "text2", ...]
}
"""
normalized_input: Final = [input] if isinstance(input, str) else input # mutable-ok: preserve list API
normalized_input: Final = [input] if isinstance(input, str) else input
return {
"model": model.removeprefix("gigachat/"),
"input": normalized_input,

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