文件历史

提交图

4 次代码提交

作者 SHA1 备注 提交日期
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
Simon Willison de63d8b69e Fixes for ruff 2026-04-22 10:13:39 -07:00
Simon Willison 65b8e37c79 Ran Black 2026-04-21 21:10:13 -07:00
Simon Willison 253fed259b Add llm/serialization.py — TypedDicts for the wire form
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>
2026-04-21 10:20:29 -07:00