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)
- API key with "clacky-" prefix (Clacky workspace key, proxied via Bedrock Converse)
- Model ID with "abs-" prefix (Clacky AI proxy that speaks Bedrock Converse)
24 25 26 |
# File 'lib/clacky/message_format/bedrock.rb', line 24 def self.bedrock_api_key?(api_key, model) api_key.to_s.start_with?("ABSK", "clacky-") || 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.
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/clacky/message_format/bedrock.rb', line 39 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)
128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/clacky/message_format/bedrock.rb', line 128 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.
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 |
# File 'lib/clacky/message_format/bedrock.rb', line 78 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 |