Module: Clacky::MessageFormat::Bedrock
- Defined in:
- lib/clacky/message_format/bedrock.rb
Overview
Static helpers for AWS Bedrock Converse API message format.
The Bedrock Converse API has a completely different format from Anthropic’s Messages API:
- Authentication: Authorization: Bearer <ABSK...key>
- Endpoint: POST /model/{modelId}/converse
- Request: { messages: [{role:, content: [{text:}]}], toolConfig: {tools: [{toolSpec:...}]}, system: [{text:}] }
- Response: { output: { message: { role:, content: [{text:} or {toolUse:}] } }, stopReason:, usage: }
Internal canonical format (same as OpenAI-style):
assistant tool_calls: { role: "assistant", tool_calls: [{id:, name:, arguments:}] }
tool result: { role: "tool", tool_call_id:, content: }
This module converts canonical format ↔ Bedrock Converse API format.
Class Method Summary collapse
-
.bedrock_api_key?(api_key, model) ⇒ Boolean
Detect if the request should use the Bedrock Converse API.
-
.build_request_body(messages, model, tools, max_tokens, caching_enabled = false) ⇒ Hash
Convert canonical @messages + tools into a Bedrock Converse 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 Bedrock Converse API response into canonical internal format.
Class Method Details
.bedrock_api_key?(api_key, model) ⇒ Boolean
Detect if the request should use the Bedrock Converse API. Matches any of:
- API key with "ABSK" prefix (native AWS Bedrock)
- Model ID with "abs-" prefix (Clacky AI proxy that speaks Bedrock Converse)
A bare “clacky-” key is NOT enough: that same workspace key is also used for dsk-*, or-*, and other OpenAI-compatible aliases served by the same Clacky proxy on a different endpoint. The *model prefix* is the source of truth for which upstream format the proxy expects:
abs-* → Bedrock Converse (POST /model/{id}/converse)
dsk-* → OpenAI-compatible (POST /chat/completions)
or-* → OpenAI-compatible (POST /chat/completions)
other → depends on base_url + explicit anthropic_format flag
Historically this method also returned true for any “clacky-” key, which forced non-abs aliases into the Bedrock endpoint and produced ‘unknown model “…”` errors. Keep the explicit-prefix rule: if you add a new OpenAI-compatible alias family on the Clacky proxy, it will route correctly without touching this file.
39 40 41 42 |
# File 'lib/clacky/message_format/bedrock.rb', line 39 def self.bedrock_api_key?(api_key, model) return true if api_key.to_s.start_with?("ABSK") model.to_s.start_with?("abs-") end |
.build_request_body(messages, model, tools, max_tokens, caching_enabled = false) ⇒ Hash
Convert canonical @messages + tools into a Bedrock Converse API request body.
55 56 57 58 59 60 61 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 87 |
# File 'lib/clacky/message_format/bedrock.rb', line 55 def build_request_body(, model, tools, max_tokens, caching_enabled = false) = .select { |m| m[:role] == "system" } = .reject { |m| m[:role] == "system" } # Merge consecutive same-role messages (Bedrock requires alternating roles) = merge_consecutive_tool_results(.map { |msg| (msg) }) # Inject cachePoint blocks AFTER conversion to Bedrock API format. # Doing this on canonical messages (before to_api_message) is incorrect because # tool-result messages (role: "tool") are converted to toolResult blocks, and # Bedrock does not support cachePoint inside toolResult.content. # Operating on the final Bedrock format ensures cachePoint is always a top-level # sibling block in the message's content array, which is what Bedrock expects. = apply_api_caching() if caching_enabled body = { messages: } # Add system prompt if present unless .empty? system_text = .map { |m| extract_text(m[:content]) }.join("\n\n") body[:system] = [{ text: system_text }] unless system_text.empty? end # Add inference config for max_tokens body[:inferenceConfig] = { maxTokens: max_tokens } # Add tool config if tools are provided if tools&.any? body[:toolConfig] = { tools: tools.map { |t| to_api_tool(t) } } end body end |
.format_tool_results(response, tool_results) ⇒ Object
Format tool results into canonical messages to append to @messages. (Same as Anthropic format — canonical tool messages)
144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/clacky/message_format/bedrock.rb', line 144 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 Bedrock Converse API response into canonical internal format.
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 |
# File 'lib/clacky/message_format/bedrock.rb', line 94 def parse_response(data) = data.dig("output", "message") || {} blocks = ["content"] || [] usage = data["usage"] || {} # Extract text content content = blocks.select { |b| b["text"] }.map { |b| b["text"] }.join("") # Extract tool calls from toolUse blocks tool_calls = blocks.select { |b| b["toolUse"] }.map do |b| tc = b["toolUse"] args = tc["input"].is_a?(String) ? tc["input"] : tc["input"].to_json { id: tc["toolUseId"], type: "function", name: tc["name"], arguments: args } end # Map Bedrock stopReason → canonical finish_reason finish_reason = case data["stopReason"] when "end_turn" then "stop" when "tool_use" then "tool_calls" when "max_tokens" then "length" else data["stopReason"] end cache_read = usage["cacheReadInputTokens"].to_i cache_write = usage["cacheWriteInputTokens"].to_i # Bedrock `inputTokens` = non-cached input only. # Anthropic direct `input_tokens` = all input including cache_read. # Normalise to Anthropic semantics so ModelPricing.calculate_cost works correctly: # prompt_tokens = inputTokens + cacheReadInputTokens # (calculate_cost subtracts cache_read_tokens from prompt_tokens to get # the billable non-cached portion; that arithmetic requires the Anthropic convention.) prompt_tokens = usage["inputTokens"].to_i + cache_read usage_data = { prompt_tokens: prompt_tokens, completion_tokens: usage["outputTokens"].to_i, total_tokens: usage["totalTokens"].to_i } usage_data[:cache_read_input_tokens] = cache_read if cache_read > 0 usage_data[:cache_creation_input_tokens] = cache_write if cache_write > 0 { content: content, tool_calls: tool_calls, finish_reason: finish_reason, usage: usage_data, raw_api_usage: usage } end |