Files
2026-07-13 12:48:46 +08:00

183 lines
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]