项目文件夹

文件
T
Simon Willison a2547d8183 Drop token_count from ReasoningPart, use redacted marker StreamEvent
ReasoningPart.token_count duplicated info already on response.token_details
(reasoning_tokens), and the side-channel `response._reasoning_token_count`
attribute with its set_usage ordering footgun was the wrong shape. Replaced
with a clean StreamEvent.redacted=True marker that plugins yield like any
other event. The framework hoists redacted reasoning Parts to the start of
the assembled message so UIs render them before content, even though the
opaque count typically arrives at the end of the stream.

Also fix parallel tool calls emitted without tool_call_id (e.g. Gemini):
a fresh tool_call_name now always allocates a new index instead of falling
through to the prior tool-call group, so N parallel calls produce N
distinct ToolCallParts instead of one with concatenated names and args.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 13:28:55 -07:00

183 行
5.1 KiB
Python

"""TypedDict spec for the JSON-safe wire form of Part, Message, and Response.
These are the exact shapes returned by ``Part.to_dict()``,
``Message.to_dict()``, and ``Response.to_dict()`` — and accepted by the
matching ``from_dict`` classmethods. They are the canonical wire format;
use them to annotate any code that reads or writes serialized llm data.
Example::
from llm.serialization import MessageDict
def save_messages(conn, messages: list[MessageDict]) -> None:
for m in messages:
conn.execute(
"INSERT INTO messages(role, parts_json) VALUES (?, ?)",
(m["role"], json.dumps(m["parts"])),
)
Or pair with Pydantic's TypeAdapter for runtime validation::
from pydantic import TypeAdapter
from llm.serialization import MessageDict
msg = TypeAdapter(MessageDict).validate_python(incoming_dict)
Or export JSON Schema for cross-language consumers::
schema = TypeAdapter(MessageDict).json_schema()
The TypedDicts are erased at runtime — zero overhead. ``NotRequired``
keys may be absent from a serialized payload; required keys must
always be present.
"""
from typing import Any, Dict, List, Literal, Union
# NotRequired moved to typing in 3.11; use typing_extensions for 3.10
# support. typing_extensions is a transitive dep via pydantic.
from typing_extensions import NotRequired, TypedDict
__all__ = [
"AttachmentDict",
"AttachmentPartDict",
"MessageDict",
"PartDict",
"PromptDict",
"ReasoningPartDict",
"ResponseDict",
"TextPartDict",
"ToolCallPartDict",
"ToolResultPartDict",
"UsageDict",
]
# ---- Attachment payload (nested inside AttachmentPartDict + tool results) ----
class AttachmentDict(TypedDict, total=False):
"""Nested attachment payload. All fields optional — an Attachment
may carry a type, a url, a path, and/or base64-encoded content.
"""
type: str
url: str
path: str
# base64-encoded bytes when the attachment was constructed with raw
# content= bytes.
content: str
# ---- Per-Part TypedDicts (discriminated by the `type` field) -----------------
class TextPartDict(TypedDict):
type: Literal["text"]
text: str
provider_metadata: NotRequired[Dict[str, Any]]
class ReasoningPartDict(TypedDict):
type: Literal["reasoning"]
text: str
# `redacted=True` with `text=""` is the marker for opaque
# reasoning (OpenAI GPT-5, Gemini without thoughts). The token
# total lives on response usage, not on the Part.
redacted: NotRequired[bool]
provider_metadata: NotRequired[Dict[str, Any]]
class ToolCallPartDict(TypedDict):
type: Literal["tool_call"]
name: str
arguments: Dict[str, Any]
tool_call_id: NotRequired[str]
# True for provider-executed calls (Anthropic web search, Gemini code
# execution). Adapters use this to restore provider-side blocks on
# the next turn.
server_executed: NotRequired[bool]
provider_metadata: NotRequired[Dict[str, Any]]
class ToolResultPartDict(TypedDict):
type: Literal["tool_result"]
name: str
output: str
tool_call_id: NotRequired[str]
server_executed: NotRequired[bool]
exception: NotRequired[str]
attachments: NotRequired[List[AttachmentDict]]
provider_metadata: NotRequired[Dict[str, Any]]
class AttachmentPartDict(TypedDict):
type: Literal["attachment"]
attachment: NotRequired[AttachmentDict]
provider_metadata: NotRequired[Dict[str, Any]]
PartDict = Union[
TextPartDict,
ReasoningPartDict,
ToolCallPartDict,
ToolResultPartDict,
AttachmentPartDict,
]
"""Discriminated union of Part dict shapes. Use with
``pydantic.TypeAdapter(PartDict)`` to validate / dispatch by ``type``.
"""
# ---- Message ----------------------------------------------------------------
class MessageDict(TypedDict):
"""JSON-safe form of ``llm.Message``.
``role`` is one of "user", "assistant", "system", "tool" in practice
— typed as ``str`` here to leave room for provider-specific values.
"""
role: str
parts: List[PartDict]
provider_metadata: NotRequired[Dict[str, Any]]
# ---- Response + nested shapes -----------------------------------------------
class PromptDict(TypedDict):
"""The ``prompt`` sub-dict of ``Response.to_dict()`` — captures the
full input chain that was sent for this turn plus any options that
apply."""
messages: List[MessageDict]
options: NotRequired[Dict[str, Any]]
system: NotRequired[str]
class UsageDict(TypedDict, total=False):
"""Optional usage block on ``ResponseDict``. All fields optional;
providers vary in which they report."""
input: int
output: int
details: Dict[str, Any]
class ResponseDict(TypedDict):
"""JSON-safe form of ``llm.Response`` — everything needed for
``Response.from_dict`` to rehydrate and ``response.reply()`` to
continue a conversation across a process boundary.
"""
model: str
prompt: PromptDict
messages: List[MessageDict]
# Audit fields — present on a freshly-serialized response, optional
# on hand-constructed ones.
id: NotRequired[str]
usage: NotRequired[UsageDict]
datetime_utc: NotRequired[str]