summaryrefslogtreecommitdiffhomepage
path: root/spec/dispatch/adapter/claude/headers_spec.rb
blob: 20ad82c0f9e0fc90617b7d4744f322bf126af052 (plain)
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