Module: Clacky::MessageFormat::Anthropic
- Defined in:
- lib/clacky/message_format/anthropic.rb
Overview
Static helpers for Anthropic API message format.
Responsibilities:
- Identify Anthropic-style messages stored in @messages
- Convert internal @messages → Anthropic API request body
- Parse Anthropic API response → internal format
- Format tool results for the next turn
Internal @messages always use OpenAI-style canonical format:
assistant tool_calls: { role: "assistant", tool_calls: [{id:, function:{name:,arguments:}}] }
tool result: { role: "tool", tool_call_id:, content: }
This module converts that canonical format to Anthropic native on the way OUT, and converts Anthropic native back to canonical on the way IN.
Class Method Summary collapse
-
.build_request_body(messages, model, tools, max_tokens, caching_enabled) ⇒ Hash
Convert canonical @messages + tools into an Anthropic API request body.
-
.format_tool_results(response, tool_results) ⇒ Object
Format tool results into canonical messages to append to @messages.
-
.parse_response(data) ⇒ Hash
Parse Anthropic API response into canonical internal format.
-
.tool_result_message?(msg) ⇒ Boolean
Returns true if the message is an Anthropic-native tool result stored in NOTE: After the refactor, new tool results are stored in canonical format (role: “tool”).
-
.tool_use_ids(msg) ⇒ Object
Returns the tool_use_ids referenced in an Anthropic-native tool result message.
Class Method Details
.build_request_body(messages, model, tools, max_tokens, caching_enabled) ⇒ Hash
Convert canonical @messages + tools into an Anthropic API request body.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/clacky/message_format/anthropic.rb', line 51 def build_request_body(, model, tools, max_tokens, caching_enabled) = .select { |m| m[:role] == "system" } = .reject { |m| m[:role] == "system" } system_text = .map { |m| extract_text(m[:content]) }.join("\n\n") = .map { |msg| (msg, caching_enabled) } api_tools = tools&.map { |t| to_api_tool(t) } if caching_enabled && api_tools&.any? api_tools.last[:cache_control] = { type: "ephemeral" } end body = { model: model, max_tokens: max_tokens, messages: } body[:system] = system_text unless system_text.empty? body[:tools] = api_tools if api_tools&.any? body end |
.format_tool_results(response, tool_results) ⇒ Object
Format tool results into canonical messages to append to @messages. Input: response (canonical, has :tool_calls), tool_results array Output: canonical messages: [{ role: “tool”, tool_call_id:, content: }]
151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/clacky/message_format/anthropic.rb', line 151 def format_tool_results(response, tool_results) results_map = tool_results.each_with_object({}) { |r, h| h[r[:id]] = r } response[:tool_calls].map do |tc| result = results_map[tc[:id]] { role: "tool", tool_call_id: tc[:id], content: result ? result[:content] : { error: "Tool result missing" }.to_json } end end |
.parse_response(data) ⇒ Hash
Parse Anthropic API response into canonical internal format.
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/clacky/message_format/anthropic.rb', line 75 def parse_response(data) blocks = data["content"] || [] usage = data["usage"] || {} content = blocks.select { |b| b["type"] == "text" }.map { |b| b["text"] }.join("") # tool_calls use canonical format (id, function: {name, arguments}) tool_calls = blocks.select { |b| b["type"] == "tool_use" }.map do |tc| args = tc["input"].is_a?(String) ? tc["input"] : tc["input"].to_json { id: tc["id"], type: "function", name: tc["name"], arguments: args } end finish_reason = case data["stop_reason"] when "end_turn" then "stop" when "tool_use" then "tool_calls" when "max_tokens" then "length" else data["stop_reason"] end # Anthropic native `input_tokens` counts ONLY the non-cached, freshly-billed # input — cache_read_input_tokens and cache_creation_input_tokens are # reported separately and are disjoint from input_tokens. # # Normalise to the codebase's canonical shape (OpenAI-style) so downstream # (ModelPricing.calculate_cost, CostTracker, show_token_usage) stays # provider-agnostic: # # prompt_tokens = non_cached + cache_read (OpenAI convention: # includes cache_read # but NOT cache_write; # ModelPricing does # `regular_input = prompt_tokens - cache_read`.) # completion_tokens = output # total_tokens = THIS TURN'S new compute volume # = raw_input + cache_creation + output # (cache_read is excluded because hits are ~free / # already-paid-for; cache_creation IS new work this # turn even though it's billed at write_rate.) # cache_read_input_tokens / cache_creation_input_tokens → independent fields # # total_tokens is purely presentational. CostTracker treats it as the # per-iteration delta directly (no subtraction of previous_total), which # is the correct reading when total_tokens already means "new work this # turn" rather than "cumulative". raw_input_tokens = usage["input_tokens"].to_i cache_read = usage["cache_read_input_tokens"].to_i cache_creation = usage["cache_creation_input_tokens"].to_i output_tokens = usage["output_tokens"].to_i prompt_tokens = raw_input_tokens + cache_read usage_data = { prompt_tokens: prompt_tokens, completion_tokens: output_tokens, # Per-turn new compute: what the server freshly processed this request. # Excludes cache_read (nearly free, already-paid-for). total_tokens: raw_input_tokens + cache_creation + output_tokens, # Signal to CostTracker: total_tokens above is already the per-turn # delta (not a running cumulative like OpenAI's). CostTracker should # NOT subtract previous_total when this flag is truthy. # OpenAI parse leaves this field unset; Bedrock may adopt the same # convention in future if we normalise it there too. total_is_per_turn: true } usage_data[:cache_read_input_tokens] = cache_read if cache_read > 0 usage_data[:cache_creation_input_tokens] = cache_creation if cache_creation > 0 { content: content, tool_calls: tool_calls, finish_reason: finish_reason, usage: usage_data, raw_api_usage: usage } end |
.tool_result_message?(msg) ⇒ Boolean
Returns true if the message is an Anthropic-native tool result stored in NOTE: After the refactor, new tool results are stored in canonical format (role: “tool”). This helper handles legacy messages that might exist in older sessions.
29 30 31 32 33 |
# File 'lib/clacky/message_format/anthropic.rb', line 29 def (msg) msg[:role] == "user" && msg[:content].is_a?(Array) && msg[:content].any? { |b| b.is_a?(Hash) && b[:type] == "tool_result" } end |
.tool_use_ids(msg) ⇒ Object
Returns the tool_use_ids referenced in an Anthropic-native tool result message.
36 37 38 39 40 |
# File 'lib/clacky/message_format/anthropic.rb', line 36 def tool_use_ids(msg) return [] unless (msg) msg[:content].select { |b| b[:type] == "tool_result" }.map { |b| b[:tool_use_id] } end |