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>
Adds a dedicated module describing the exact JSON-safe shape returned
by Part.to_dict() / Message.to_dict() / Response.to_dict() and accepted
by the matching from_dict methods. Every consumer that reads or writes
serialized llm data can now import a specific TypedDict and get proper
autocomplete, static type-checking, and schema generation support.
Module: llm/serialization.py (deliberately not "schema" — that name is
taken by the structured-output feature).
TextPartDict, ReasoningPartDict, ToolCallPartDict,
ToolResultPartDict, AttachmentPartDict
— one per Part subclass, each discriminated by a
Literal["<type>"] on the `type` field so pydantic/type-checkers
can narrow cleanly.
PartDict = Union[...]
— the discriminated-union form of all Part dicts.
AttachmentDict — the nested attachment payload (base64 content when
bytes were supplied).
MessageDict — {role, parts: list[PartDict], provider_metadata?}
PromptDict, UsageDict, ResponseDict — full Response.to_dict() shape
including the input chain, options, messages, and audit fields.
TypedDicts use typing_extensions.NotRequired (available for 3.10+ via
a transitive pydantic dep) so Python 3.10 consumers work.
Type annotations on every .to_dict() / .from_dict() method across
parts.py and models.py now reference the specific TypedDict rather
than Dict[str, Any]. Consumers writing
def save_messages(msgs: list[MessageDict]) -> None: ...
get autocomplete on msgs[i]["role"], type-errors on typos, and pydantic
TypeAdapter-based validation works out of the box:
from pydantic import TypeAdapter
from llm.serialization import MessageDict
TypeAdapter(MessageDict).validate_python(incoming) # validate
TypeAdapter(MessageDict).json_schema() # export
Also tidied _response_to_dict to omit usage.details when None so the
serialized UsageDict doesn't carry a null field where pydantic would
reject it during validation.
New test_serialization.py (41 tests):
- required/optional key sets on every TypedDict
- actual .to_dict() output conforms to its TypedDict via TypeAdapter
- PartDict discriminated union accepts all 5 Part variants and
rejects unknown types
- Literal discriminator values are correct
- method annotations point at the right TypedDicts
- JSON round-trip of Response.to_dict() validates
661 total tests pass (620 before + 41 new). llm-anthropic (32) and
llm-gemini (50) still green against the editable llm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>