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
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
|
# frozen_string_literal: true
RSpec.describe Dispatch::Adapter::Claude::Headers do
let(:oauth_token) { "sk-ant-oat01-abc123" }
let(:api_key) { "sk-ant-api03-xyz789" }
describe ".build" do
context "with OAuth token" do
subject(:headers) { described_class.build(api_key: oauth_token) }
it "emits Authorization: Bearer ..." do
expect(headers["Authorization"]).to eq("Bearer #{oauth_token}")
end
it "does not emit X-Api-Key" do
expect(headers).not_to have_key("X-Api-Key")
end
it "sets User-Agent to the Claude CLI user agent" do
expect(headers["User-Agent"]).to eq(Dispatch::Adapter::Claude::Headers::USER_AGENT)
end
it "sets Content-Type to application/json" do
expect(headers["Content-Type"]).to eq("application/json")
end
it "sets Accept to application/json for non-streaming" do
expect(headers["Accept"]).to eq("application/json")
end
it "sets Accept to text/event-stream for streaming" do
h = described_class.build(api_key: oauth_token, stream: true)
expect(h["Accept"]).to eq("text/event-stream")
end
it "includes anthropic-version header" do
expect(headers["anthropic-version"]).to eq("2023-06-01")
end
end
context "with raw API key" do
subject(:headers) { described_class.build(api_key: api_key) }
it "emits X-Api-Key" do
expect(headers["X-Api-Key"]).to eq(api_key)
end
it "does not emit Authorization" do
expect(headers).not_to have_key("Authorization")
end
it "does not set User-Agent" do
expect(headers).not_to have_key("User-Agent")
end
end
context "with explicit is_oauth: true override" do
it "treats the key as OAuth even when it doesn't start with sk-ant-oat" do
h = described_class.build(api_key: "some-other-token", is_oauth: true)
expect(h["Authorization"]).to eq("Bearer some-other-token")
expect(h).not_to have_key("X-Api-Key")
end
end
context "with explicit is_oauth: false override" do
it "treats an oat token as API key" do
h = described_class.build(api_key: oauth_token, is_oauth: false)
expect(h["X-Api-Key"]).to eq(oauth_token)
expect(h).not_to have_key("Authorization")
end
end
context "Anthropic-Beta header" do
it "includes all DEFAULT_BETAS" do
h = described_class.build(api_key: api_key)
beta_values = h["anthropic-beta"].split(",")
Dispatch::Adapter::Claude::Headers::DEFAULT_BETAS.each do |b|
expect(beta_values).to include(b)
end
end
it "includes interleaved-thinking beta by default" do
h = described_class.build(api_key: api_key)
expect(h["anthropic-beta"]).to include(
Dispatch::Adapter::Claude::Headers::INTERLEAVED_THINKING_BETA
)
end
it "omits interleaved-thinking beta when interleaved_thinking: false" do
h = described_class.build(api_key: api_key, interleaved_thinking: false)
expect(h["anthropic-beta"]).not_to include(
Dispatch::Adapter::Claude::Headers::INTERLEAVED_THINKING_BETA
)
end
it "includes extra_betas in the beta header" do
h = described_class.build(api_key: api_key, extra_betas: ["my-beta-2025-01-01"])
expect(h["anthropic-beta"]).to include("my-beta-2025-01-01")
end
it "deduplicates beta values" do
dup_beta = Dispatch::Adapter::Claude::Headers::DEFAULT_BETAS.first
h = described_class.build(api_key: api_key, extra_betas: [dup_beta])
values = h["anthropic-beta"].split(",")
expect(values.count(dup_beta)).to eq(1)
end
end
context "with caller extra headers" do
it "includes extra headers" do
h = described_class.build(api_key: api_key, extra: { "X-Custom" => "value" })
expect(h["X-Custom"]).to eq("value")
end
it "caller extra does NOT clobber Authorization for OAuth tokens" do
h = described_class.build(
api_key: oauth_token,
extra: { "Authorization" => "Bearer malicious" }
)
expect(h["Authorization"]).to eq("Bearer #{oauth_token}")
end
it "caller extra does NOT clobber X-Api-Key for API keys" do
h = described_class.build(
api_key: api_key,
extra: { "X-Api-Key" => "stolen-key" }
)
expect(h["X-Api-Key"]).to eq(api_key)
end
it "accepts extra keys as symbols and converts them to strings" do
h = described_class.build(api_key: api_key, extra: { "X-Custom": "value" })
expect(h["X-Custom"]).to eq("value")
end
end
context "Stainless metadata headers" do
subject(:headers) { described_class.build(api_key: api_key) }
it "includes x-stainless-lang: ruby" do
expect(headers["x-stainless-lang"]).to eq("ruby")
end
it "includes x-stainless-package-version" do
expect(headers["x-stainless-package-version"]).to eq(
Dispatch::Adapter::Claude::Headers::STAINLESS_PACKAGE_VERSION
)
end
it "includes x-stainless-runtime: ruby" do
expect(headers["x-stainless-runtime"]).to eq("ruby")
end
it "includes x-stainless-runtime-version matching RUBY_VERSION" do
expect(headers["x-stainless-runtime-version"]).to eq(RUBY_VERSION)
end
end
end
end
|