mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-19 00:01:29 +00:00
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:
parent
08a78a3982
commit
0246769138
284 changed files with 1479 additions and 2254 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 ("}", "]")
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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=[])
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]: ...
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue