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, reasoning_effort: nil) ⇒ Hash
Convert canonical @messages + tools into an Anthropic API request body.
-
.format_tool_results(response, tool_results) ⇒ Object
── Tool result formatting ──────────────────────────────────────────────── Format tool results into canonical messages to append to @messages.
-
.parse_response(data) ⇒ Hash
Parse Anthropic API response into canonical internal format.
-
.sanitize_tool_use_id(id) ⇒ Object
Anthropic requires tool_use.id to match ^[a-zA-Z0-9_-]+$ (max 128 chars).
-
.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, reasoning_effort: nil) ⇒ Hash
Convert canonical @messages + tools into an Anthropic API request body.
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
# File 'lib/clacky/message_format/anthropic.rb', line 62 def build_request_body(, model, tools, max_tokens, caching_enabled, reasoning_effort: nil) = .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) } = merge_consecutive_tool_results() 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? if (effort = normalized_effort(reasoning_effort)) body[:thinking] = { type: "adaptive" } body[:output_config] = { effort: effort } end body end |
.format_tool_results(response, tool_results) ⇒ Object
── Tool result formatting ──────────────────────────────────────────────── 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: }]
202 203 204 205 206 207 208 209 210 211 212 213 |
# File 'lib/clacky/message_format/anthropic.rb', line 202 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.
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/clacky/message_format/anthropic.rb', line 127 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 |
.sanitize_tool_use_id(id) ⇒ Object
Anthropic requires tool_use.id to match ^[a-zA-Z0-9_-]+$ (max 128 chars). Some OpenAI-compatible upstreams (e.g. kimi-k2.6) return ids like "tool_name:0" — fine for OpenAI, rejected by Anthropic. We replace illegal chars with "_" at the format boundary so ids stay self-consistent across use/result pairs (pure function → same input maps to same output in both directions).
47 48 49 50 51 |
# File 'lib/clacky/message_format/anthropic.rb', line 47 def sanitize_tool_use_id(id) s = id.to_s s = s.gsub(/[^a-zA-Z0-9_-]/, "_") s.length > 128 ? s[0, 128] : s 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 |