# frozen_string_literal: true module Dispatch module Adapter class Claude < Base # Converts an Anthropic JSON response Hash (already parsed) into a # Dispatch::Adapter::Response together with its Usage / UsageCost. # # Entry point: # ResponseBuilder.build(json, model_info:, is_oauth:) #=> Response module ResponseBuilder # Stop-reason mapping from Anthropic strings to interface symbols. STOP_REASON_MAP = { "end_turn" => :end_turn, "max_tokens" => :max_tokens, "tool_use" => :tool_use, "pause_turn" => :pause_turn, "refusal" => :refusal, "sensitive" => :sensitive, "stop_sequence" => :end_turn # we never send stop sequences }.freeze module_function # Build a Response from a parsed Anthropic JSON body. # # @param json [Hash] the parsed response body # @param model_info [ModelInfo] used for pricing # @param is_oauth [Boolean] strip proxy_ prefix from tool_use names # @return [Dispatch::Adapter::Response] def build(json, model_info:, is_oauth:) content_blocks, tool_calls = parse_content(json["content"] || [], is_oauth: is_oauth) stop_reason = map_stop_reason(json["stop_reason"]) usage = build_usage(json["usage"] || {}, model_info: model_info) model_id = json["model"] || model_info&.id Response.new( content: content_blocks, tool_calls: tool_calls, model: model_id, stop_reason: stop_reason, usage: usage ) end # Parse the content array from the Anthropic response. # Returns [content_blocks, tool_calls] where: # content_blocks = [TextBlock, ThinkingBlock, RedactedThinkingBlock, …] # tool_calls = [ToolUseBlock, …] def parse_content(content_array, is_oauth:) blocks = [] tool_calls = [] Array(content_array).each do |item| next unless item.is_a?(Hash) type = item["type"].to_s case type when "text" text = item["text"].to_s blocks << TextBlock.new(text: text) unless text.empty? when "thinking" thinking = item["thinking"].to_s signature = item["signature"].to_s blocks << ThinkingBlock.new(thinking: thinking, signature: signature.empty? ? nil : signature) when "redacted_thinking" data = item["data"].to_s blocks << RedactedThinkingBlock.new(data: data) unless data.empty? when "tool_use" name = item["name"].to_s name = Cloaking.strip_prefix(name) if is_oauth tool_calls << ToolUseBlock.new( id: item["id"].to_s, name: name, arguments: item["input"] || {} ) end end [blocks, tool_calls] end # ── Stop-reason mapping ────────────────────────────────────────────── def map_stop_reason(raw) STOP_REASON_MAP.fetch(raw.to_s, :end_turn) end # ── Usage / cost building ──────────────────────────────────────────── def build_usage(usage_hash, model_info:) input_tokens = usage_hash["input_tokens"].to_i output_tokens = usage_hash["output_tokens"].to_i cache_read_tokens = usage_hash["cache_read_input_tokens"].to_i cache_creation_tokens = usage_hash["cache_creation_input_tokens"].to_i usage = Usage.new( input_tokens: input_tokens, output_tokens: output_tokens, cache_read_tokens: cache_read_tokens, cache_creation_tokens: cache_creation_tokens ) usage.cost = Pricing.calculate(usage, model_info) usage end end end end end