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.
-
.normalise_prompt_tokens(input_tokens, cache_read, cache_creation) ⇒ Object
Normalise a /v1/messages usage block to the codebase's canonical (OpenAI-style) prompt_tokens: total input including cache_read but excluding cache_creation.
-
.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.
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 |
# File 'lib/clacky/message_format/anthropic.rb', line 81 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? # Kimi (Moonshot Coding Plan) models routed through the Anthropic # /v1/messages format expect thinking:{type:"enabled"} — the native # Anthropic "adaptive" type is Claude-specific and is silently ignored # by the Kimi backend, so thinking never actually activates on K3. # # K3 additionally supports a "max" effort level (strongest reasoning) # beyond the standard low/medium/high triple. The generic # normalized_effort() helper drops "max" because it only whitelists # the standard three levels, making K3's signature strongest-reasoning # mode unreachable through this format adapter. # # This branch only changes K3's wire format; every other model keeps # using the adaptive path below unchanged. When reasoning_effort is # nil/empty we leave the body untouched (provider default), and we # never emit output_config for "on" so the backend can pick its own # default — both behaviours match the official kimi CLI. if model.to_s.match?(/\A(kimi-)?k3/i) effort_str = reasoning_effort.to_s k3_effort = case effort_str when "max", "xhigh" then "max" when "high", "medium", "low" then effort_str else nil end body[:thinking] = { type: "enabled" } body[:output_config] = { effort: k3_effort } if k3_effort elsif (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: }]
242 243 244 245 246 247 248 249 250 251 252 253 |
# File 'lib/clacky/message_format/anthropic.rb', line 242 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 |
.normalise_prompt_tokens(input_tokens, cache_read, cache_creation) ⇒ Object
Normalise a /v1/messages usage block to the codebase's canonical (OpenAI-style) prompt_tokens: total input including cache_read but excluding cache_creation.
Anthropic reports input_tokens as the post-breakpoint tail only, with the cache buckets disjoint from it. Some /v1/messages-compatible gateways instead report input_tokens as the FULL input with both cache buckets already folded in; adding cache_read there would bill the whole cached prefix twice. Detect that by whether the cache buckets already fit inside input_tokens.
32 33 34 35 36 37 38 39 |
# File 'lib/clacky/message_format/anthropic.rb', line 32 def normalise_prompt_tokens(input_tokens, cache_read, cache_creation) cached_input = cache_read + cache_creation if cached_input.positive? && input_tokens >= cached_input input_tokens - cache_creation else input_tokens + cache_read end end |
.parse_response(data) ⇒ Hash
Parse Anthropic API response into canonical internal format.
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 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/clacky/message_format/anthropic.rb', line 172 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 # Normalise to the codebase's canonical shape (OpenAI-style) so downstream # (ModelPricing.calculate_cost, CostTracker, show_token_usage) stays # provider-agnostic: # # prompt_tokens = total input incl. cache_read, excl. cache_write # (ModelPricing does # `regular_input = prompt_tokens - cache_read`.) # completion_tokens = output # total_tokens = THIS TURN'S new compute volume # = non_cached + 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 = normalise_prompt_tokens(raw_input_tokens, cache_read, cache_creation) 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). Derived from the # normalised prompt_tokens so it stays correct under both conventions. total_tokens: (prompt_tokens - cache_read) + 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).
66 67 68 69 70 |
# File 'lib/clacky/message_format/anthropic.rb', line 66 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.
48 49 50 51 52 |
# File 'lib/clacky/message_format/anthropic.rb', line 48 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.
55 56 57 58 59 |
# File 'lib/clacky/message_format/anthropic.rb', line 55 def tool_use_ids(msg) return [] unless (msg) msg[:content].select { |b| b[:type] == "tool_result" }.map { |b| b[:tool_use_id] } end |