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 |
# 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) } 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: }]
173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/clacky/message_format/anthropic.rb', line 173 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.
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 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/clacky/message_format/anthropic.rb', line 98 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 |