Repository navigation
Add Message dict conversion and multi-part tool messages - #79
Conversation
Message.to_dict()/from_dict() convert messages to JSON-compatible dicts. A single text part is stored as a plain content string; other parts use a typed list. Tool results are now `tool` messages built with republic.tool(call, ...), whose output may mix text, images, and videos. This replaces ToolResult, tool_result(), and assistant(tool_results=...). Unannounced calls are still added to the assistant turn before their results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve conflicts with native audio input: chat tool messages reuse the format's audio encoder, Gemini tool responses carry audio parts, and Message dict conversion handles audio parts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Blocking finding: a Gemini tool result that carries media as a URL is serialized outside the API schema.
republic.tool(call, "Captured", republic.image("https://…/shot.png")) reaches the google provider as functionResponse.parts[].fileData, which is not part of Gemini's FunctionResponsePart, so that round trip never delivers the image. Media carried as bytes is unaffected. The inline comment has the emitted body, the schema evidence and a repair direction.
Non-blocking question on the chat format: OpenAI's Chat Completions schema for a tool message (ChatCompletionRequestToolMessage.content) says "For tool messages, only type text is supported", so the image_url part this PR emits for a media tool result is rejected by OpenAI and by strict OpenAI-compatible servers. If that is intentional for gateways such as OpenRouter, the "Media support in tool results depends on the provider and API format" line added in docs/guides/tools.md may be enough; otherwise the chat format needs the same guard the other formats have.
Gemini's FunctionResponsePart accepts only inlineData, so a tool result with URL media would be serialized as fileData outside the schema. Raise UnsupportedFeatureError before sending instead, and document the media limits of tool results. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
On the Chat Completions question: emitting |
There was a problem hiding this comment.
Blocking finding: normalize() no longer drops assistant turns that carry no parts and no tool calls, so the Messages and Gemini formats now send a request body their APIs reject.
The rewrite replaced the old guard (if message.parts or calls:) with an unconditional normalized.append(message). A refusal or an empty completion that republic wrote to history is therefore echoed back as an empty assistant turn, and the next request in that conversation fails instead of continuing. The inline comment has the emitted bodies, a reproduction on this revision and a one-line repair verified against the test suite, lint and type checks.
An assistant turn with no parts and no tool calls, such as a refusal written to history, was sent back after the normalize() rewrite. Anthropic and Gemini reject empty content, so the next request failed. Filter those turns out after announcing calls, restoring the previous request bodies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
No blocking findings.
The findings from my earlier reviews of this branch are addressed on this revision, including the two commits pushed afterwards; the normalize() one is verified in its thread.
Summary
Add
Message.to_dict()andMessage.from_dict(). A message with a single text part stores it as a plaincontentstring; any other parts use a typed list (text,reasoning,image,video,provider_data). Media data is base64-encoded, so the dict round-trips through JSON.Breaking: tool results are now
toolmessages built withrepublic.tool(call, *content, is_error=False). The output may mix text, images, and videos. This replacesToolResult,republic.tool_result(), andassistant(tool_results=...).Messagegainstool_callandis_error.tool_callis required ontoolmessages and rejected on other roles.normalize()still announces calls that do not appear earlier in the conversation, adding them to the assistant turn before the run of tool messages.Multi-part tool output per format:
role: "tool"content becomes a content list.function_call_output.outputbecomes aninput_text/input_imagelist.tool_result.contentbecomes a block list.response, and media goes infunctionResponse.parts.Text-only output produces the same request bodies as before.
Migration
Tests
uv run prek run --all-filesuv run ty checkuv run pytest(449 passed)🤖 Generated with Claude Code