Refs https://github.com/simonw/llm/pull/1411
7.7 KiB
(serialization)=
Serialization wire format
LLM provides JSON-safe serialization for Response, Message, and Part objects through to_dict() / from_dict() methods. The exact shapes of the resulting dicts are defined as TypedDicts in the llm.serialization module.
These TypedDicts:
- Annotate every
to_dict()/from_dict()method, so static type-checkers, IDE autocomplete, and pydantic'sTypeAdapterwork out of the box. - Document the keys that may appear in each payload and which are required.
- Are erased at runtime — they have zero overhead and no extra dependencies.
(serialization-using)=
Using the TypedDicts
Annotate functions that produce or consume serialized payloads:
import json
from pathlib import Path
import llm
from llm.serialization import ResponseDict
def store_turn(payload: ResponseDict) -> None:
Path("turn.json").write_text(json.dumps(payload))
model = llm.get_model("gpt-5.4-mini")
response = model.prompt("Hi")
response.text()
store_turn(response.to_dict())
Validate untrusted payloads at runtime via pydantic:
from pydantic import TypeAdapter
from llm.serialization import MessageDict
incoming = json.loads(some_payload)
validated = TypeAdapter(MessageDict).validate_python(incoming)
Or export a JSON Schema for cross-language consumers:
schema = TypeAdapter(MessageDict).json_schema()
(serialization-reference)=
Reference
The Part shapes are listed first, since they nest inside the rest. Required keys must be present in every payload; optional keys may be omitted.
AttachmentDict
Nested attachment payload. All fields optional — an Attachment may carry a type, a url, a path, and/or base64-encoded content.
typeoptionalstrurloptionalstrpathoptionalstrcontentoptionalstr
TextPartDict
typerequiredLiteral['text']textrequiredstrprovider_metadataoptionalDict[str, Any]
ReasoningPartDict
typerequiredLiteral['reasoning']textrequiredstrredactedoptionalboolprovider_metadataoptionalDict[str, Any]
ToolCallPartDict
typerequiredLiteral['tool_call']namerequiredstrargumentsrequiredDict[str, Any]tool_call_idoptionalstrserver_executedoptionalboolprovider_metadataoptionalDict[str, Any]
ToolResultPartDict
typerequiredLiteral['tool_result']namerequiredstroutputrequiredstrtool_call_idoptionalstrserver_executedoptionalboolexceptionoptionalstrattachmentsoptionalList[AttachmentDict]provider_metadataoptionalDict[str, Any]
AttachmentPartDict
typerequiredLiteral['attachment']attachmentoptionalAttachmentDictprovider_metadataoptionalDict[str, Any]
PartDict
Discriminated union of all Part dict shapes — every value of type maps to exactly one TypedDict above.
type: "text"TextPartDicttype: "reasoning"ReasoningPartDicttype: "tool_call"ToolCallPartDicttype: "tool_result"ToolResultPartDicttype: "attachment"AttachmentPartDict
MessageDict
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.
rolerequiredstrpartsrequiredList[TextPartDict | ReasoningPartDict | ToolCallPartDict | ToolResultPartDict | AttachmentPartDict]provider_metadataoptionalDict[str, Any]
PromptDict
The prompt sub-dict of Response.to_dict() — captures the
full input chain that was sent for this turn plus any options that
apply.
messagesrequiredList[MessageDict]optionsoptionalDict[str, Any]systemoptionalstr
UsageDict
Optional usage block on ResponseDict. All fields optional;
providers vary in which they report.
inputoptionalintoutputoptionalintdetailsoptionalDict[str, Any]
ResponseDict
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.
modelrequiredstrpromptrequiredPromptDictmessagesrequiredList[MessageDict]idoptionalstrusageoptionalUsageDictdatetime_utcoptionalstr
(serialization-notes)=
Notes
- All TypedDicts use
NotRequired[...]for optional keys (viatyping_extensions, which is a transitive dependency through pydantic). On Python 3.11+ this comes from the standard librarytypingmodule. AttachmentDict.contentis base64-encoded when the attachment was constructed from raw bytes — that's how binary attachments survive the JSON round-trip.ResponseDict.id,usage, anddatetime_utcare present on freshly serialized responses but optional on hand-constructed ones —Response.from_dict()will accept either.- The
provider_metadatakey on every Part and Message is opaque by design. It carries provider-specific signatures (Anthropic extended-thinking signatures, GeminithoughtSignature, OpenAIencrypted_content) that need to round-trip verbatim across turns. See the "Restoring opaque metadata on subsequent requests" section in {doc}plugins/advanced-model-pluginsfor how plugins use this on the wire.