mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
290 lines
9.6 KiB
Python
290 lines
9.6 KiB
Python
import json
|
|
from collections.abc import Mapping
|
|
from types import MappingProxyType
|
|
from typing import Annotated, Final, Literal
|
|
|
|
from integration._support.client import object_value
|
|
from integration.translation.chat_completions.cross_model.contracts import (
|
|
CacheCreationDetails,
|
|
Contract,
|
|
NonemptyString,
|
|
TokenCount,
|
|
)
|
|
from pydantic import Field, JsonValue, TypeAdapter
|
|
|
|
JSON_ARGUMENTS: Final = TypeAdapter(dict[str, JsonValue])
|
|
|
|
|
|
class TextBlock(Contract):
|
|
type: Literal["text"]
|
|
text: str
|
|
citations: list[dict[str, JsonValue]] | None = None
|
|
|
|
|
|
class ThinkingBlock(Contract):
|
|
type: Literal["thinking"]
|
|
thinking: str
|
|
signature: NonemptyString
|
|
|
|
|
|
class RedactedThinkingBlock(Contract):
|
|
type: Literal["redacted_thinking"]
|
|
data: NonemptyString
|
|
|
|
|
|
class ToolUseBlock(Contract):
|
|
type: Literal["tool_use"]
|
|
id: NonemptyString
|
|
name: NonemptyString
|
|
input: dict[str, JsonValue]
|
|
caller: dict[str, JsonValue] | None = None
|
|
|
|
|
|
MessageBlock = Annotated[TextBlock | ThinkingBlock | RedactedThinkingBlock | ToolUseBlock, Field(discriminator="type")]
|
|
|
|
|
|
class MessageOutputDetails(Contract):
|
|
thinking_tokens: TokenCount | None = None
|
|
|
|
|
|
class MessageServerToolUse(Contract):
|
|
web_search_requests: TokenCount | None = None
|
|
web_fetch_requests: TokenCount | None = None
|
|
|
|
|
|
class MessageUsage(Contract):
|
|
input_tokens: TokenCount
|
|
output_tokens: TokenCount
|
|
cache_creation_input_tokens: TokenCount | None = None
|
|
cache_read_input_tokens: TokenCount | None = None
|
|
cache_creation: CacheCreationDetails | None = None
|
|
output_tokens_details: MessageOutputDetails | None = None
|
|
server_tool_use: MessageServerToolUse | None = None
|
|
service_tier: str | None = None
|
|
inference_geo: str | None = None
|
|
|
|
|
|
class MessagesResponse(Contract):
|
|
id: NonemptyString
|
|
type: Literal["message"]
|
|
role: Literal["assistant"]
|
|
model: NonemptyString
|
|
content: Annotated[list[MessageBlock], Field(min_length=1)]
|
|
stop_reason: Literal["end_turn", "tool_use", "max_tokens", "stop_sequence", "pause_turn", "refusal"]
|
|
stop_sequence: str | None
|
|
usage: MessageUsage
|
|
container: dict[str, JsonValue] | None = None
|
|
diagnostics: dict[str, JsonValue] | None = None
|
|
stop_details: dict[str, JsonValue] | None = None
|
|
|
|
|
|
class OutputText(Contract):
|
|
type: Literal["output_text"]
|
|
text: str
|
|
annotations: list[dict[str, JsonValue]]
|
|
logprobs: list[dict[str, JsonValue]] | None = None
|
|
|
|
|
|
class OutputMessage(Contract):
|
|
type: Literal["message"]
|
|
id: NonemptyString
|
|
status: Literal["completed", "incomplete", "in_progress"]
|
|
role: Literal["assistant"]
|
|
content: Annotated[list[OutputText], Field(min_length=1)]
|
|
phase: str | None = None
|
|
|
|
|
|
class ReasoningSummary(Contract):
|
|
type: Literal["summary_text"]
|
|
text: str
|
|
|
|
|
|
class OutputReasoning(Contract):
|
|
type: Literal["reasoning"]
|
|
id: NonemptyString
|
|
summary: list[ReasoningSummary] | None = None
|
|
content: list[OutputText] | None = None
|
|
encrypted_content: str | None = None
|
|
|
|
|
|
class OutputFunctionCall(Contract):
|
|
type: Literal["function_call"]
|
|
id: NonemptyString
|
|
call_id: NonemptyString
|
|
name: NonemptyString
|
|
arguments: str
|
|
status: Literal["completed", "incomplete", "in_progress"] | None = None
|
|
namespace: str | None = None
|
|
|
|
|
|
OutputItem = Annotated[OutputMessage | OutputReasoning | OutputFunctionCall, Field(discriminator="type")]
|
|
|
|
|
|
class InputTokenDetails(Contract):
|
|
cached_tokens: TokenCount | None = None
|
|
cache_write_tokens: TokenCount | None = None
|
|
text_tokens: TokenCount | None = None
|
|
audio_tokens: TokenCount | None = None
|
|
image_tokens: TokenCount | None = None
|
|
video_tokens: TokenCount | None = None
|
|
|
|
|
|
class OutputTokenDetails(Contract):
|
|
reasoning_tokens: TokenCount | None = None
|
|
text_tokens: TokenCount | None = None
|
|
audio_tokens: TokenCount | None = None
|
|
|
|
|
|
class ResponsesUsage(Contract):
|
|
input_tokens: TokenCount
|
|
output_tokens: TokenCount
|
|
total_tokens: TokenCount
|
|
input_tokens_details: InputTokenDetails | None = None
|
|
output_tokens_details: OutputTokenDetails | None = None
|
|
|
|
|
|
class ResponsesResponse(Contract):
|
|
id: NonemptyString
|
|
object: Literal["response"]
|
|
created_at: Annotated[int | float, Field(ge=0)]
|
|
status: Literal["completed", "incomplete", "failed", "in_progress", "cancelled", "queued"]
|
|
error: dict[str, JsonValue] | None
|
|
incomplete_details: dict[str, JsonValue] | None
|
|
instructions: str | None
|
|
metadata: dict[str, str]
|
|
model: NonemptyString
|
|
output: Annotated[list[OutputItem], Field(min_length=1)]
|
|
parallel_tool_calls: bool
|
|
tool_choice: str | dict[str, JsonValue]
|
|
tools: list[dict[str, JsonValue]]
|
|
usage: ResponsesUsage
|
|
max_output_tokens: TokenCount | None = None
|
|
previous_response_id: str | None = None
|
|
reasoning: dict[str, JsonValue] | None = None
|
|
store: bool | None = None
|
|
temperature: float | None = None
|
|
text: dict[str, JsonValue] | None = None
|
|
top_p: float | None = None
|
|
truncation: str | None = None
|
|
user: str | None = None
|
|
background: bool | None = None
|
|
completed_at: Annotated[int | float, Field(ge=0)] | None = None
|
|
frequency_penalty: float | None = None
|
|
max_tool_calls: TokenCount | None = None
|
|
presence_penalty: float | None = None
|
|
prompt_cache_key: str | None = None
|
|
prompt_cache_retention: str | None = None
|
|
safety_identifier: str | None = None
|
|
service_tier: str | None = None
|
|
top_logprobs: TokenCount | None = None
|
|
provider_specific_fields: dict[str, JsonValue] | None = None
|
|
access_programs: dict[str, JsonValue] | None = None
|
|
billing: dict[str, JsonValue] | None = None
|
|
moderation: dict[str, JsonValue] | None = None
|
|
tool_usage: dict[str, JsonValue] | None = None
|
|
|
|
|
|
MESSAGES_OPTIONAL: Final = MappingProxyType(
|
|
{
|
|
"": frozenset(("container", "diagnostics", "stop_details")),
|
|
"content[]": frozenset(("caller", "citations")),
|
|
"usage": frozenset(
|
|
(
|
|
"cache_creation_input_tokens",
|
|
"cache_read_input_tokens",
|
|
"cache_creation",
|
|
"output_tokens_details",
|
|
"server_tool_use",
|
|
"service_tier",
|
|
"inference_geo",
|
|
"speed",
|
|
)
|
|
),
|
|
}
|
|
)
|
|
RESPONSES_OPTIONAL: Final = MappingProxyType(
|
|
{
|
|
"": frozenset(
|
|
(
|
|
"parallel_tool_calls",
|
|
"tool_choice",
|
|
"tools",
|
|
"max_output_tokens",
|
|
"previous_response_id",
|
|
"reasoning",
|
|
"store",
|
|
"temperature",
|
|
"text",
|
|
"top_p",
|
|
"truncation",
|
|
"user",
|
|
"background",
|
|
"completed_at",
|
|
"frequency_penalty",
|
|
"max_tool_calls",
|
|
"presence_penalty",
|
|
"prompt_cache_key",
|
|
"prompt_cache_retention",
|
|
"safety_identifier",
|
|
"service_tier",
|
|
"top_logprobs",
|
|
"provider_specific_fields",
|
|
"access_programs",
|
|
"billing",
|
|
"moderation",
|
|
"tool_usage",
|
|
)
|
|
),
|
|
"output[]": frozenset(("namespace", "status", "phase")),
|
|
"output[].content[]": frozenset(("logprobs",)),
|
|
"usage": frozenset(("input_tokens_details", "output_tokens_details", "cost")),
|
|
}
|
|
)
|
|
|
|
|
|
def messages_structure(body: Mapping[str, JsonValue]) -> JsonValue:
|
|
response: Final = MessagesResponse.model_validate(body)
|
|
tools: Final = tuple(block for block in response.content if isinstance(block, ToolUseBlock))
|
|
assert len({tool.id for tool in tools}) == len(tools), body
|
|
assert response.stop_reason == ("tool_use" if tools else "end_turn"), body
|
|
return _structure(dict(body), "", MESSAGES_OPTIONAL, "content", "tool_use")
|
|
|
|
|
|
def responses_structure(body: Mapping[str, JsonValue]) -> JsonValue:
|
|
response: Final = ResponsesResponse.model_validate(body)
|
|
assert response.status == "completed", body
|
|
assert response.error is None and response.incomplete_details is None, body
|
|
assert response.usage.total_tokens == response.usage.input_tokens + response.usage.output_tokens, body
|
|
tools: Final = tuple(item for item in response.output if isinstance(item, OutputFunctionCall))
|
|
assert len({tool.call_id for tool in tools}) == len(tools), body
|
|
assert len({item.id for item in response.output}) == len(response.output), body
|
|
for tool in tools:
|
|
JSON_ARGUMENTS.validate_json(tool.arguments)
|
|
return _structure(dict(body), "", RESPONSES_OPTIONAL, "output", "function_call")
|
|
|
|
|
|
def _structure(
|
|
value: JsonValue,
|
|
path: str,
|
|
optional: Mapping[str, frozenset[str]],
|
|
items_path: str,
|
|
tool_type: str,
|
|
) -> JsonValue:
|
|
if isinstance(value, dict):
|
|
excluded: Final = optional.get(path, frozenset[str]())
|
|
return {
|
|
key: _structure(item, f"{path}.{key}" if path else key, optional, items_path, tool_type)
|
|
for key, item in value.items()
|
|
if key not in excluded
|
|
}
|
|
if isinstance(value, list):
|
|
tools: Final = tuple(
|
|
item for item in value if path == items_path and object_value(item).get("type") == tool_type
|
|
)
|
|
compared: Final = tools or tuple(value)
|
|
shapes: Final = tuple(_structure(item, f"{path}[]", optional, items_path, tool_type) for item in compared)
|
|
ordered: Final = tuple(sorted({json.dumps(shape, sort_keys=True) for shape in shapes}))
|
|
return [TypeAdapter(JsonValue).validate_json(shape) for shape in ordered]
|
|
if path == "created_at":
|
|
return "number"
|
|
return type(value).__name__
|