chore(lint): clear the new LIT001/LIT002 violations and ratchet the lint budgets

The type-discipline gate flagged 17 new mutable-collection annotations and 31
new mutable-collection constructions added by this branch. Replace raw dict
literals with the OpenAI SDK's TypedDict call forms, annotate read-only params
as Mapping/Sequence, precompute the custom tool call id set as a frozenset,
and accumulate streamed arguments as tuples. The few places where a plain
list/dict is a hard contract (pydantic response fields, fastapi route tags,
parsed request bodies, in-place tool call patching) carry reasoned mutable-ok
suppressions instead. Ratchet the ruff, type-discipline, and basedpyright
budgets down by the violations this branch now fixes on net
This commit is contained in:
mateo-berri 2026-08-01 16:22:05 -07:00
parent c27f1b7b6d
commit 075babd00f
14 changed files with 227 additions and 153 deletions

View file

@ -42,7 +42,7 @@
"limit": 18
},
"reportIndexIssue": {
"limit": 37
"limit": 36
},
"reportInvalidTypeForm": {
"limit": 35
@ -114,10 +114,10 @@
"limit": 31978
},
"reportUnnecessaryCast": {
"limit": 177
"limit": 175
},
"reportUnnecessaryComparison": {
"limit": 1021
"limit": 1019
},
"reportUnnecessaryContains": {
"limit": 7

View file

@ -4,6 +4,7 @@ Handler for transforming /chat/completions api requests to litellm.responses req
import json
import os
from collections.abc import Mapping
from typing import (
TYPE_CHECKING,
Any,
@ -21,6 +22,13 @@ from typing import (
)
from openai.types.responses.custom_tool_param import CustomToolParam
from openai.types.responses.response_input_param import (
FunctionCallOutput,
ResponseCustomToolCallOutputParam,
ResponseCustomToolCallParam,
)
from openai.types.responses.tool_choice_custom_param import ToolChoiceCustomParam
from openai.types.responses.tool_choice_function_param import ToolChoiceFunctionParam
from openai.types.responses.tool_param import FunctionToolParam
from pydantic import BaseModel
@ -40,6 +48,8 @@ from litellm.responses.utils import normalize_responses_api_stream_options
from litellm.types.llms.openai import (
ChatCompletionAnnotation,
ChatCompletionReasoningItem,
ChatCompletionToolCallChunk,
ChatCompletionToolCallFunctionChunk,
ChatCompletionToolParamFunctionChunk,
Reasoning,
ResponsesAPIOptionalRequestParams,
@ -101,7 +111,11 @@ def _build_reasoning_item(
}
def _tool_call_dict_from_output_item(item: dict[str, Any]) -> dict[str, Any]:
class _ChatToolCallDict(ChatCompletionToolCallChunk, total=False):
provider_specific_fields: Mapping[str, Any]
def _tool_call_dict_from_output_item(item: Mapping[str, Any], index: int) -> _ChatToolCallDict:
"""Convert a ``function_call`` or ``custom_tool_call`` output item dict to a chat
completions tool_call dict. Custom (grammar/freeform) tool calls carry their raw
string payload in ``input`` rather than ``arguments``; both map to
@ -115,22 +129,32 @@ def _tool_call_dict_from_output_item(item: dict[str, Any]) -> dict[str, Any]:
is_custom = item.get("type") == "custom_tool_call"
arguments = (item.get("input") if is_custom else item.get("arguments")) or ""
name = item.get("name") or ("custom_tool" if is_custom else "")
tool_call_dict: dict[str, Any] = {
"id": LiteLLMCompletionResponsesConfig._tool_call_id_from_responses_item(item.get("id"), item.get("call_id")),
"function": {"name": name, "arguments": arguments},
"type": "function",
}
provider_specific_fields = item.get("provider_specific_fields")
if provider_specific_fields and not isinstance(provider_specific_fields, dict):
provider_specific_fields = (
dict(provider_specific_fields) if hasattr(provider_specific_fields, "__dict__") else None
)
function_chunk = ChatCompletionToolCallFunctionChunk(name=name, arguments=arguments)
tool_call_dict = _ChatToolCallDict(
id=LiteLLMCompletionResponsesConfig._tool_call_id_from_responses_item(item.get("id"), item.get("call_id")),
type="function",
function=function_chunk,
index=index,
)
raw_provider_fields = item.get("provider_specific_fields")
if isinstance(raw_provider_fields, dict):
provider_specific_fields = raw_provider_fields
elif raw_provider_fields and hasattr(raw_provider_fields, "__dict__"):
provider_specific_fields = vars(raw_provider_fields)
else:
provider_specific_fields = None
if provider_specific_fields:
tool_call_dict["provider_specific_fields"] = provider_specific_fields
tool_call_dict["function"]["provider_specific_fields"] = provider_specific_fields
function_chunk["provider_specific_fields"] = provider_specific_fields
return tool_call_dict
def _flat_responses_tool_choice(choice_type: str, name: str) -> Union[ToolChoiceFunctionParam, ToolChoiceCustomParam]:
if choice_type == "custom":
return ToolChoiceCustomParam(type="custom", name=name)
return ToolChoiceFunctionParam(type="function", name=name)
def _reasoning_item_to_response_input(
r_item: Union[ChatCompletionReasoningItem, Dict[str, Any]],
) -> Dict[str, Any]:
@ -163,12 +187,12 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
return tool_choice
if isinstance(tool_choice.get("name"), str) and tool_choice.get("name"):
# Return only Responses shape so stray chat ``function``/``custom`` keys are not sent upstream.
return {"type": choice_type, "name": tool_choice["name"]}
return _flat_responses_tool_choice(choice_type, tool_choice["name"])
nested = tool_choice.get(choice_type)
if isinstance(nested, dict):
nested_name = nested.get("name")
if isinstance(nested_name, str) and nested_name:
return {"type": choice_type, "name": nested_name}
return _flat_responses_tool_choice(choice_type, nested_name)
return tool_choice
def _handle_raw_dict_response_item(self, item: Dict[str, Any], index: int) -> Tuple[Optional[Any], int]:
@ -221,7 +245,15 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
) -> Tuple[List[Any], Optional[str]]:
input_items: List[Any] = []
instructions: Optional[str] = None
custom_tool_call_ids: set = set()
custom_tool_call_ids = frozenset(
tool_call["id"]
for msg in messages
if msg.get("role") == "assistant" and isinstance(msg.get("tool_calls"), list)
for tool_call in msg.get("tool_calls") or ()
if isinstance(tool_call, dict)
and not tool_call.get("function")
and isinstance(tool_call.get("custom"), dict)
)
for msg in messages:
role = msg.get("role")
@ -269,19 +301,19 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
tool_output = [{"type": "input_text", "text": str(content)}]
if tool_call_id in custom_tool_call_ids:
input_items.append(
{
"type": "custom_tool_call_output",
"call_id": tool_call_id,
"output": content if isinstance(content, str) else tool_output,
}
ResponseCustomToolCallOutputParam(
type="custom_tool_call_output",
call_id=tool_call_id,
output=content if isinstance(content, str) else tool_output,
)
)
else:
input_items.append(
{
"type": "function_call_output",
"call_id": tool_call_id,
"output": tool_output,
}
FunctionCallOutput(
type="function_call_output",
call_id=tool_call_id,
output=tool_output,
)
)
elif role == "assistant" and tool_calls and isinstance(tool_calls, list):
for r_item in _get_reasoning_items(msg):
@ -300,14 +332,13 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
input_tool_call["arguments"] = function["arguments"]
input_items.append(input_tool_call)
elif isinstance(custom, dict):
custom_tool_call_ids.add(tool_call["id"])
input_items.append(
{
"type": "custom_tool_call",
"call_id": tool_call["id"],
"name": custom.get("name", ""),
"input": custom.get("input", ""),
}
ResponseCustomToolCallParam(
type="custom_tool_call",
call_id=tool_call["id"],
name=custom.get("name", ""),
input=custom.get("input", ""),
)
)
else:
raise ValueError(f"tool call not supported: {tool_call}")
@ -598,7 +629,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
# Tool calls accumulate into the single trailing tool_calls choice
# like the typed branches above; a choice per call would hide every
# call after choices[0] from chat clients
accumulated_tool_calls.append(_tool_call_dict_from_output_item(raw_item))
accumulated_tool_calls.append(_tool_call_dict_from_output_item(raw_item, tool_call_index))
tool_call_index += 1
elif handle_raw_dict_callback is not None:
choice, index = handle_raw_dict_callback(item=raw_item, index=index)
@ -925,10 +956,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
)
custom_payload = tool["custom"]
flat_custom: CustomToolParam = {
"type": "custom",
"name": custom_payload.get("name", ""),
}
flat_custom = CustomToolParam(type="custom", name=custom_payload.get("name", ""))
if custom_payload.get("description") is not None:
flat_custom["description"] = custom_payload["description"]
if isinstance(custom_payload.get("format"), dict):
@ -1130,7 +1158,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
def __init__(self, streaming_response, sync_stream: bool, json_mode: Optional[bool] = False):
super().__init__(streaming_response, sync_stream, json_mode)
self._chat_completion_id: str | None = None
self._tool_call_index_map: dict[int, int] = {}
self._tool_call_index_map: dict[int, int] = {} # mutable-ok: per-stream accumulator state
def _handle_string_chunk(
self, str_line: Union[str, "BaseModel"]
@ -1151,7 +1179,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
@staticmethod
def _sequential_tool_call_index(
tool_call_index_map: dict[int, int] | None,
tool_call_index_map: dict[int, int] | None, # mutable-ok: per-stream state, remapped in place
output_index: int,
) -> int:
"""Chat-completions tool_call indices must be 0-based and sequential, but
@ -1170,7 +1198,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
@staticmethod
def translate_responses_chunk_to_openai_stream(
parsed_chunk: Union[dict, BaseModel],
tool_call_index_map: dict[int, int] | None = None,
tool_call_index_map: dict[int, int] | None = None, # mutable-ok: per-stream state, remapped in place
) -> "ModelResponseStream":
"""
Translate a Responses API streaming chunk to OpenAI chat completion streaming format.
@ -1229,7 +1257,7 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
# New output item added
output_item = parsed_chunk.get("item", {})
if output_item.get("type") in ("function_call", "custom_tool_call"):
converted = _tool_call_dict_from_output_item(output_item)
converted = _tool_call_dict_from_output_item(output_item, parsed_chunk.get("output_index", 0))
provider_specific_fields = converted.get("provider_specific_fields")
function_chunk = ChatCompletionToolCallFunctionChunk(
@ -1299,16 +1327,15 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
# tool call; per-stream callers already received it via
# output_item.added and the argument delta events
return ModelResponseStream(
choices=[
choices=[ # mutable-ok: ModelResponseStream coerces only list choices
StreamingChoices(
index=0,
delta=Delta(
tool_calls=[
{
**_tool_call_dict_from_output_item(dict(output_item)),
"index": parsed_chunk.get("output_index", 0),
}
]
tool_calls=(
_tool_call_dict_from_output_item(
output_item, parsed_chunk.get("output_index", 0)
),
)
),
finish_reason=None,
)

View file

@ -61,24 +61,19 @@ class HeliconeLogger:
for tool_call in message["tool_calls"]:
function = tool_call.get("function")
custom = tool_call.get("custom")
if function:
content.append(
{
"type": "tool_use",
"id": tool_call["id"],
"name": function["name"],
"input": function["arguments"],
}
)
elif custom:
content.append(
{
"type": "tool_use",
"id": tool_call["id"],
"name": custom["name"],
"input": custom["input"],
}
)
if not function and not custom:
continue
name, tool_input = (
(function["name"], function["arguments"]) if function else (custom["name"], custom["input"])
)
content.append(
{
"type": "tool_use",
"id": tool_call["id"],
"name": name,
"input": tool_input,
}
)
elif "content" in message and message["content"]:
content = [{"type": "text", "text": message["content"]}]

View file

@ -22,25 +22,18 @@ def parse_tool_calls(tool_calls):
def clean_tool_call(tool_call):
custom = getattr(tool_call, "custom", None)
if custom is not None:
return {
"type": tool_call.type,
"id": tool_call.id,
"function": {
"name": custom.name,
"arguments": custom.input,
},
}
serialized = {
name, arguments = custom.name, custom.input
else:
name, arguments = tool_call.function.name, tool_call.function.arguments
return {
"type": tool_call.type,
"id": tool_call.id,
"function": {
"name": tool_call.function.name,
"arguments": tool_call.function.arguments,
"name": name,
"arguments": arguments,
},
}
return serialized
return [
clean_tool_call(tool_call)
for tool_call in tool_calls

View file

@ -3,6 +3,7 @@ import json
import re
import time
import traceback
from collections.abc import Sequence
from typing import Dict, Iterable, List, Literal, Optional, Tuple, Union, cast
import litellm
@ -371,7 +372,9 @@ from collections import defaultdict
def _handle_invalid_parallel_tool_calls(
tool_calls: List[Union[ChatCompletionMessageToolCall, ChatCompletionMessageCustomToolCall]],
tool_calls: List[
Union[ChatCompletionMessageToolCall, ChatCompletionMessageCustomToolCall]
], # mutable-ok: patched in place via slice assignment
):
"""
Handle hallucinated parallel tool call from openai - https://community.openai.com/t/model-tries-to-call-unknown-function-multi-tool-use-parallel/490653
@ -532,7 +535,7 @@ class LiteLLMResponseObjectHandler:
def _should_convert_tool_call_to_json_mode(
tool_calls: (
list[ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall] | list[DatabricksTool] | None
Sequence[ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall] | Sequence[DatabricksTool] | None
) = None,
convert_tool_call_to_json_mode: Optional[bool] = None,
) -> bool:

View file

@ -21,6 +21,14 @@ from typing import (
cast,
)
from openai.types.chat.chat_completion_custom_tool_param import (
CustomFormatGrammar,
CustomFormatGrammarGrammar,
)
from openai.types.shared_params.custom_tool_input_format import (
Grammar as ResponsesGrammarFormat,
)
import litellm
from litellm import verbose_logger
from litellm.router_utils.batch_utils import InMemoryFile
@ -1252,29 +1260,36 @@ def is_function_call(optional_params: dict) -> bool:
return False
def convert_custom_tool_format_to_chat_shape(format_obj: dict) -> dict:
def convert_custom_tool_format_to_chat_shape(format_obj: Mapping[str, Any]) -> Mapping[str, Any]:
"""
Responses API grammar formats are flat ({"type": "grammar", "definition", "syntax"});
Chat Completions wraps the same fields in a "grammar" object. Text formats are
identical on both surfaces and pass through, as does anything unrecognized.
"""
if format_obj.get("type") == "grammar" and "grammar" not in format_obj:
return {
"type": "grammar",
"grammar": {k: format_obj[k] for k in ("definition", "syntax") if k in format_obj},
}
return format_obj
if format_obj.get("type") != "grammar" or "grammar" in format_obj:
return format_obj
grammar = CustomFormatGrammarGrammar()
if "definition" in format_obj:
grammar["definition"] = format_obj["definition"]
if "syntax" in format_obj:
grammar["syntax"] = format_obj["syntax"]
return CustomFormatGrammar(type="grammar", grammar=grammar)
def convert_custom_tool_format_to_responses_shape(format_obj: dict) -> dict:
def convert_custom_tool_format_to_responses_shape(format_obj: Mapping[str, Any]) -> Mapping[str, Any]:
"""
Inverse of convert_custom_tool_format_to_chat_shape: unwrap the Chat Completions
"grammar" object into the flat Responses API grammar shape.
"""
grammar = format_obj.get("grammar")
if format_obj.get("type") == "grammar" and isinstance(grammar, dict):
return {"type": "grammar", **{k: grammar[k] for k in ("definition", "syntax") if k in grammar}}
return format_obj
if format_obj.get("type") != "grammar" or not isinstance(grammar, dict):
return format_obj
flat = ResponsesGrammarFormat(type="grammar")
if "definition" in grammar:
flat["definition"] = grammar["definition"]
if "syntax" in grammar:
flat["syntax"] = grammar["syntax"]
return flat
def get_file_ids_from_messages(messages: List[AllMessageValues]) -> List[str]:

View file

@ -1,5 +1,6 @@
import base64
import time
from collections.abc import Mapping, Sequence
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast
from litellm.types.llms.openai import (
@ -205,9 +206,13 @@ class ChunkProcessor:
return response
def get_combined_tool_content(
self, tool_call_chunks: List[Dict[str, Any]]
) -> List[Union[ChatCompletionMessageToolCall, ChatCompletionMessageCustomToolCall]]:
tool_calls_list: List[Union[ChatCompletionMessageToolCall, ChatCompletionMessageCustomToolCall]] = []
self, tool_call_chunks: Sequence[Mapping[str, Any]]
) -> List[
Union[ChatCompletionMessageToolCall, ChatCompletionMessageCustomToolCall]
]: # mutable-ok: assigned verbatim to Message.tool_calls, a List field
tool_calls_list: List[
Union[ChatCompletionMessageToolCall, ChatCompletionMessageCustomToolCall]
] = [] # mutable-ok: see return type
tool_call_map: Dict[int, Dict[str, Any]] = {} # Map to store tool calls by index
for chunk in tool_call_chunks:
@ -245,9 +250,9 @@ class ChunkProcessor:
"id": None,
"name": None,
"type": None,
"arguments": [],
"arguments": (),
"custom_name": None,
"custom_input": [],
"custom_input": (),
"provider_specific_fields": None,
}
@ -263,20 +268,20 @@ class ChunkProcessor:
if function.get("name"):
tool_call_map[index]["name"] = function["name"]
if function.get("arguments"):
tool_call_map[index]["arguments"].append(function["arguments"])
tool_call_map[index]["arguments"] += (function["arguments"],)
else:
# function is an object
if hasattr(function, "name") and function.name:
tool_call_map[index]["name"] = function.name
if hasattr(function, "arguments") and function.arguments:
tool_call_map[index]["arguments"].append(function.arguments)
tool_call_map[index]["arguments"] += (function.arguments,)
custom = tool_call.get("custom")
if isinstance(custom, dict):
if custom.get("name"):
tool_call_map[index]["custom_name"] = custom["name"]
if custom.get("input"):
tool_call_map[index]["custom_input"].append(custom["input"])
tool_call_map[index]["custom_input"] += (custom["input"],)
else:
# tool_call is an object
if hasattr(tool_call, "id") and tool_call.id:
@ -287,14 +292,14 @@ class ChunkProcessor:
if hasattr(tool_call.function, "name") and tool_call.function.name:
tool_call_map[index]["name"] = tool_call.function.name
if hasattr(tool_call.function, "arguments") and tool_call.function.arguments:
tool_call_map[index]["arguments"].append(tool_call.function.arguments)
tool_call_map[index]["arguments"] += (tool_call.function.arguments,)
custom = getattr(tool_call, "custom", None)
if custom is not None:
if getattr(custom, "name", None):
tool_call_map[index]["custom_name"] = custom.name
if getattr(custom, "input", None):
tool_call_map[index]["custom_input"].append(custom.input)
tool_call_map[index]["custom_input"] += (custom.input,)
# Preserve provider_specific_fields from streaming chunks
provider_fields = None

View file

@ -533,12 +533,14 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig):
for choice in choices:
## HANDLE JSON MODE - anthropic returns single function call]
tool_calls = choice["message"].get("tool_calls", None)
new_tool_calls: list[ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall] | None = None
new_tool_calls: list[ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall] | None = (
None # mutable-ok: holds _handle_invalid_parallel_tool_calls' list; Message.__init__ expects list
)
message_content = choice["message"].get("content", None)
if tool_calls is not None:
_openai_tool_calls = []
for _tc in tool_calls:
_openai_tc = chat_completion_tool_call_from_dict(dict(_tc))
_openai_tc = chat_completion_tool_call_from_dict(_tc)
_openai_tool_calls.append(_openai_tc)
fixed_tool_calls = _handle_invalid_parallel_tool_calls(_openai_tool_calls)

View file

@ -1058,7 +1058,7 @@ def responses_api_bridge_check(
# summary alias is present with ``reasoning_effort`` (tools alone stay on chat).
has_function_tool = any(
(tool.get("type") == "function" if isinstance(tool, dict) else getattr(tool, "type", None) == "function")
for tool in (tools or [])
for tool in (tools or ())
)
if isinstance(reasoning_effort, dict):
reasoning_active = reasoning_effort.get("effort") != "none" or reasoning_effort.get("summary") is not None

View file

@ -1,6 +1,8 @@
import asyncio
import json
import time
from collections.abc import Mapping
from types import MappingProxyType
from typing import Any, AsyncIterator, Dict, Optional, cast
from uuid import uuid4
@ -23,19 +25,30 @@ from litellm.types.responses.main import DeleteResponseResult
router = APIRouter()
_user_api_key_auth_dep = Depends(user_api_key_auth)
_RESPONSES_TAGS = ["responses"] # mutable-ok: fastapi's route signature requires List[str] tags
_TOOL_PAYLOAD_KEYS = {
"custom": ("name", "description", "format"),
"function": ("name", "description", "parameters", "strict"),
}
_TOOL_PAYLOAD_KEYS: Mapping[str, tuple[str, ...]] = MappingProxyType(
{
"custom": ("name", "description", "format"),
"function": ("name", "description", "parameters", "strict"),
}
)
_EMPTY_TOOL_PAYLOAD: Mapping[str, Any] = MappingProxyType({})
def _convert_tool_envelope(obj: object, *, to_chat: bool) -> object:
def _convert_tool_payload_value(key: str, value: object, *, to_chat: bool) -> object:
if key != "format" or not isinstance(value, dict):
return value
from litellm.litellm_core_utils.prompt_templates.common_utils import (
convert_custom_tool_format_to_chat_shape,
convert_custom_tool_format_to_responses_shape,
)
convert = convert_custom_tool_format_to_chat_shape if to_chat else convert_custom_tool_format_to_responses_shape
return convert(value)
def _convert_tool_envelope(obj: object, *, to_chat: bool) -> object:
if not isinstance(obj, dict):
return obj
tool_type = obj.get("type")
@ -43,35 +56,37 @@ def _convert_tool_envelope(obj: object, *, to_chat: bool) -> object:
if payload_keys is None:
return obj
nested = obj.get(tool_type)
nested_source = nested if isinstance(nested, dict) else {}
payload = {
key: nested_source[key] if key in nested_source else obj[key]
nested_source = nested if isinstance(nested, dict) else _EMPTY_TOOL_PAYLOAD
payload = { # mutable-ok: tool entries are embedded verbatim in the JSON request body
key: _convert_tool_payload_value(key, nested_source[key] if key in nested_source else obj[key], to_chat=to_chat)
for key in payload_keys
if key in nested_source or key in obj
}
if "name" not in payload:
return obj
if isinstance(payload.get("format"), dict):
convert = convert_custom_tool_format_to_chat_shape if to_chat else convert_custom_tool_format_to_responses_shape
payload = {**payload, "format": convert(payload["format"])}
return {"type": tool_type, tool_type: payload} if to_chat else {"type": tool_type, **payload}
return {"type": tool_type, tool_type: payload} if to_chat else {"type": tool_type, **payload} # mutable-ok: same
def _normalize_tool_dialect(data: dict, *, to_chat: bool) -> dict:
converted: dict = {}
def _normalize_tool_dialect(
data: dict, *, to_chat: bool
) -> dict: # mutable-ok: the parsed request body contract is a plain dict
tools = data.get("tools")
if isinstance(tools, list):
normalized_tools = [_convert_tool_envelope(tool, to_chat=to_chat) for tool in tools]
if normalized_tools != tools:
converted["tools"] = normalized_tools
tool_choice = data.get("tool_choice")
normalized_tools = (
[
_convert_tool_envelope(tool, to_chat=to_chat) for tool in tools
] # mutable-ok: body's tools stays a plain JSON list
if isinstance(tools, list)
else tools
)
normalized_choice = _convert_tool_envelope(tool_choice, to_chat=to_chat)
if normalized_choice != tool_choice:
converted["tool_choice"] = normalized_choice
return {**data, **converted} if converted else data
if normalized_tools == tools and normalized_choice == tool_choice:
return data
replaceable = (("tools", normalized_tools), ("tool_choice", normalized_choice))
return {**data, **{key: value for key, value in replaceable if key in data}} # mutable-ok: plain body dict
def _is_chat_completions_body(data: dict) -> bool:
def _is_chat_completions_body(data: Mapping[str, Any]) -> bool:
messages = data.get("messages")
if isinstance(messages, list) and len(messages) > 0:
return True
@ -340,13 +355,13 @@ async def responses_api(
@router.get(
"/cursor/models",
dependencies=[Depends(user_api_key_auth)],
tags=["responses"],
dependencies=(_user_api_key_auth_dep,),
tags=_RESPONSES_TAGS,
)
@router.get(
"/cursor/v1/models",
dependencies=[Depends(user_api_key_auth)],
tags=["responses"],
dependencies=(_user_api_key_auth_dep,),
tags=_RESPONSES_TAGS,
)
async def cursor_model_list(
user_api_key_dict: UserAPIKeyAuth = _user_api_key_auth_dep,
@ -447,7 +462,7 @@ async def cursor_chat_completions(
# Rebuild rather than pop: _read_request_body can return the request-scope
# cached parsed-body dict itself, and removing keys from it corrupts the
# cache's key snapshot so later readers get an empty body
data = {key: value for key, value in data.items() if key != "stream_options"}
data = {key: value for key, value in data.items() if key != "stream_options"} # mutable-ok: plain body dict
data = _normalize_tool_dialect(data, to_chat=False)

View file

@ -7,6 +7,12 @@ import re
from collections.abc import Sequence
from typing import Any, Literal, cast
from openai.types.chat.chat_completion_named_tool_choice_param import (
ChatCompletionNamedToolChoiceParam,
)
from openai.types.chat.chat_completion_named_tool_choice_param import (
Function as NamedToolChoiceFunction,
)
from openai.types.responses import ResponseFunctionToolCall
from openai.types.responses.response_create_params import ResponseInputParam
from openai.types.responses.tool_param import FunctionToolParam
@ -160,13 +166,17 @@ class LiteLLMCompletionResponsesConfig:
elif tool_choice_type == "function":
function_name = tool_choice.get("name")
if function_name:
return {"type": "function", "function": {"name": function_name}}
return ChatCompletionNamedToolChoiceParam(
type="function", function=NamedToolChoiceFunction(name=function_name)
)
return "required"
elif tool_choice_type == "custom":
custom = tool_choice.get("custom")
custom_name = tool_choice.get("name") or (custom.get("name") if isinstance(custom, dict) else None)
if custom_name:
return {"type": "function", "function": {"name": custom_name}}
return ChatCompletionNamedToolChoiceParam(
type="function", function=NamedToolChoiceFunction(name=custom_name)
)
return "required"
# Return as-is for unknown formats

View file

@ -1,6 +1,7 @@
import json
import time
from enum import Enum
from types import MappingProxyType
from typing import (
TYPE_CHECKING,
Any,
@ -1162,16 +1163,16 @@ class ChatCompletionMessageToolCall(OpenAIObject):
setattr(self, key, value)
def is_custom_tool_call_dict(tool_call: dict) -> bool:
def is_custom_tool_call_dict(tool_call: Mapping[str, Any]) -> bool:
return tool_call.get("type") == "custom" or tool_call.get("custom") is not None
def chat_completion_tool_call_from_dict(
tool_call: dict,
tool_call: Mapping[str, Any],
) -> "ChatCompletionMessageToolCall | ChatCompletionMessageCustomToolCall":
if is_custom_tool_call_dict(tool_call):
return ChatCompletionMessageCustomToolCall(
**{k: v for k, v in tool_call.items() if not (k in ("function", "type") and v is None)}
**MappingProxyType({k: v for k, v in tool_call.items() if not (k in ("function", "type") and v is None)})
)
return ChatCompletionMessageToolCall(**tool_call)
@ -1228,7 +1229,9 @@ def add_provider_specific_fields(object: BaseModel, provider_specific_fields: Op
class Message(SafeAttributeModel, OpenAIObject):
content: Optional[str]
role: Literal["assistant", "user", "system", "tool", "function"]
tool_calls: Optional[List[Union[ChatCompletionMessageToolCall, ChatCompletionMessageCustomToolCall]]]
tool_calls: Optional[
List[Union[ChatCompletionMessageToolCall, ChatCompletionMessageCustomToolCall]]
] # mutable-ok: public pydantic response field; only the union member is new
function_call: Optional[FunctionCall]
audio: Optional[ChatCompletionAudioResponse] = None
images: Optional[List[ImageURLListItem]] = None
@ -1352,7 +1355,9 @@ class Delta(SafeAttributeModel, OpenAIObject):
content: Optional[str]
role: Optional[str]
function_call: Optional[FunctionCall]
tool_calls: Optional[List[Union[ChatCompletionDeltaToolCall, ChatCompletionDeltaCustomToolCall]]]
tool_calls: Optional[
List[Union[ChatCompletionDeltaToolCall, ChatCompletionDeltaCustomToolCall]]
] # mutable-ok: public pydantic response field; only the union member is new
audio: Optional[ChatCompletionAudioResponse]
images: Optional[List[ImageURLListItem]]
annotations: Optional[List[ChatCompletionAnnotation]]
@ -1389,8 +1394,10 @@ class Delta(SafeAttributeModel, OpenAIObject):
if function_call is not None and isinstance(function_call, dict):
function_call = FunctionCall(**function_call)
if tool_calls is not None and isinstance(tool_calls, list):
coerced_tool_calls: List[Union[ChatCompletionDeltaToolCall, ChatCompletionDeltaCustomToolCall]] = []
if tool_calls is not None and isinstance(tool_calls, (list, tuple)):
coerced_tool_calls: List[
Union[ChatCompletionDeltaToolCall, ChatCompletionDeltaCustomToolCall]
] = [] # mutable-ok: public Delta.tool_calls contract is a list
current_index = 0
for tool_call in tool_calls:
if isinstance(tool_call, dict):
@ -1400,7 +1407,9 @@ class Delta(SafeAttributeModel, OpenAIObject):
if is_custom_tool_call_dict(tool_call):
coerced_tool_calls.append(
ChatCompletionDeltaCustomToolCall(
**{k: v for k, v in tool_call.items() if not (k == "function" and v is None)}
**MappingProxyType(
{k: v for k, v in tool_call.items() if not (k == "function" and v is None)}
)
)
)
else:

View file

@ -135,7 +135,7 @@
"limit": 30
},
"PERF401": {
"limit": 142
"limit": 141
},
"PERF402": {
"limit": 9
@ -222,7 +222,7 @@
"limit": 38
},
"RET504": {
"limit": 702
"limit": 701
},
"RUF010": {
"limit": 874
@ -267,7 +267,7 @@
"limit": 324
},
"SIM103": {
"limit": 129
"limit": 128
},
"SIM113": {
"limit": 6
@ -324,7 +324,7 @@
"limit": 879
},
"UP006": {
"limit": 12050
"limit": 12045
},
"UP007": {
"limit": 2526
@ -363,6 +363,6 @@
"limit": 104
},
"UP045": {
"limit": 17793
"limit": 17791
}
}

View file

@ -1,9 +1,9 @@
{
"LIT001": {
"limit": 23191
"limit": 23180
},
"LIT002": {
"limit": 27276
"limit": 27259
},
"LIT003": {
"limit": 292
@ -24,6 +24,6 @@
"limit": 1004
},
"LIT009": {
"limit": 2467
"limit": 2465
}
}