1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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
72
73
74
75
76
77
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
|
# 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
|