Module: Mistri::Providers::Anthropic::Serializer
- Defined in:
- lib/mistri/providers/anthropic/serializer.rb
Overview
Serializes protocol messages into Anthropic Messages API wire shapes.
Wire rules that matter: consecutive tool results merge into one user turn (parallel tool calls demand their results together), thinking blocks replay with their signature and must never be altered, redacted thinking replays as its opaque payload, and cache_control marks the last system block and the last user message so the stable prefix and the growing history both cache.
Class Method Summary collapse
-
.block(block) ⇒ Object
Returns nil for a block the API would reject (empty text, unusable thinking), so callers filter_map it out.
- .mark_last_user_turn(wire) ⇒ Object
- .message(msg) ⇒ Object
- .messages(history, cache: false) ⇒ Object
- .system_blocks(system, cache:) ⇒ Object
-
.text_block(block) ⇒ Object
The API rejects empty text content blocks.
-
.thinking_block(block) ⇒ Object
Thinking replays only with its signature.
- .tool_results(group) ⇒ Object
- .tools(definitions) ⇒ Object
Class Method Details
.block(block) ⇒ Object
Returns nil for a block the API would reject (empty text, unusable thinking), so callers filter_map it out.
60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/mistri/providers/anthropic/serializer.rb', line 60 def block(block) case block when Content::Text then text_block(block) when Content::Thinking then thinking_block(block) when Content::Image { type: "image", source: { type: "base64", media_type: block.mime_type, data: block.data } } when ToolCall { type: "tool_use", id: block.id, name: block.name, input: block.arguments } else raise SchemaError, "cannot serialize #{block.class} for Anthropic" end end |
.mark_last_user_turn(wire) ⇒ Object
93 94 95 96 97 98 99 100 101 102 |
# File 'lib/mistri/providers/anthropic/serializer.rb', line 93 def mark_last_user_turn(wire) last_user = wire.rindex { |turn| turn[:role] == "user" } return unless last_user content = wire[last_user][:content] return unless content.is_a?(Array) && content.any? content.last[:cache_control] = { type: "ephemeral" } end |
.message(msg) ⇒ Object
44 45 46 |
# File 'lib/mistri/providers/anthropic/serializer.rb', line 44 def (msg) { role: msg.role.to_s, content: msg.content.filter_map { |block| block(block) } } end |
.messages(history, cache: false) ⇒ Object
25 26 27 28 29 30 31 32 |
# File 'lib/mistri/providers/anthropic/serializer.rb', line 25 def (history, cache: false) turns = history.reject(&:system?).chunk_while { |a, b| a.tool? && b.tool? } wire = turns.map do |group| group.first.tool? ? tool_results(group) : (group.first) end mark_last_user_turn(wire) if cache wire end |
.system_blocks(system, cache:) ⇒ Object
17 18 19 20 21 22 23 |
# File 'lib/mistri/providers/anthropic/serializer.rb', line 17 def system_blocks(system, cache:) return nil if system.nil? || system.empty? blocks = [{ type: "text", text: system }] blocks.last[:cache_control] = { type: "ephemeral" } if cache blocks end |
.text_block(block) ⇒ Object
The API rejects empty text content blocks.
75 76 77 |
# File 'lib/mistri/providers/anthropic/serializer.rb', line 75 def text_block(block) block.text.empty? ? nil : { type: "text", text: block.text } end |
.thinking_block(block) ⇒ Object
Thinking replays only with its signature. Redacted thinking carries its opaque payload; a normal thinking block missing its signature (an aborted turn cut before signature_delta) cannot replay, so it degrades to its text, or drops when even that is empty.
83 84 85 86 87 88 89 90 91 |
# File 'lib/mistri/providers/anthropic/serializer.rb', line 83 def thinking_block(block) return { type: "redacted_thinking", data: block.signature } if block.redacted? if block.signature return { type: "thinking", thinking: block.thinking, signature: block.signature } end block.thinking.empty? ? nil : { type: "text", text: block.thinking } end |
.tool_results(group) ⇒ Object
48 49 50 51 52 53 54 55 56 |
# File 'lib/mistri/providers/anthropic/serializer.rb', line 48 def tool_results(group) { role: "user", content: group.map do |msg| blocks = msg.content.filter_map { |block| block(block) } # The API rejects an empty tool_result; a space stands in for a # tool that returned nothing. blocks = [{ type: "text", text: " " }] if blocks.empty? { type: "tool_result", tool_use_id: msg.tool_call_id, content: blocks } end } end |
.tools(definitions) ⇒ Object
34 35 36 37 38 39 40 41 42 |
# File 'lib/mistri/providers/anthropic/serializer.rb', line 34 def tools(definitions) definitions.map do |tool| spec = tool.transform_keys(&:to_sym) wire = { name: spec[:name], description: spec[:description], input_schema: spec[:input_schema] } wire[:eager_input_streaming] = true if spec[:eager_input_streaming] wire end end |