mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
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:
parent
c27f1b7b6d
commit
075babd00f
14 changed files with 227 additions and 153 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"]}]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue