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
|
# frozen_string_literal: true
module Dispatch
module Adapter
class Claude < Base
module Headers
CLAUDE_CODE_VERSION = "2.1.63"
STAINLESS_PACKAGE_VERSION = "0.74.0"
DEFAULT_BETAS = %w[
claude-code-20250219
oauth-2025-04-20
context-management-2025-06-27
prompt-caching-scope-2026-01-05
].freeze
INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14"
USER_AGENT = "claude-cli/#{CLAUDE_CODE_VERSION} (external, cli)".freeze
module_function
# Build the full set of request headers.
#
# @param api_key [String] OAuth access token or raw API key
# @param is_oauth [Boolean, nil] nil = auto-detect from token prefix
# @param stream [Boolean] set Accept to text/event-stream when true
# @param extra_betas [Array<String>] additional beta header values
# @param interleaved_thinking [Boolean] include the interleaved-thinking beta
# @param base_url [String] (unused in headers but kept for symmetry)
# @param extra [Hash] low-precedence caller overrides
# @param claude_code_only [Boolean] when true, omit SDK-identifying
# headers (anthropic-version, x-stainless-*) so the request looks
# like real Claude Code rather than the Anthropic SDK. Required
# for /api/oauth/* endpoints which reject SDK-style requests with
# "OAuth authentication is currently not supported". Also adds the
# accept-encoding and connection headers that Claude Code sends.
# @return [Hash<String,String>]
def build(
api_key:,
is_oauth: nil,
stream: false,
extra_betas: [],
interleaved_thinking: true,
base_url: "https://api.anthropic.com", # rubocop:disable Lint/UnusedMethodArgument
extra: {},
claude_code_only: false
)
oauth = is_oauth.nil? ? api_key.to_s.start_with?("sk-ant-oat") : is_oauth
# Start with the lowest-priority caller extras (these can be
# clobbered by anything below).
headers = extra.transform_keys(&:to_s)
if claude_code_only
# Real Claude Code header set — no SDK identifiers.
# NOTE: do NOT set `accept-encoding` here. Net::HTTP only
# auto-decompresses responses when it added the accept-encoding
# header itself; setting it manually leaves us with raw gzip
# bytes that JSON.parse cannot handle.
headers["connection"] = "keep-alive"
else
# Stainless / SDK metadata headers
headers.merge!(
"anthropic-version" => "2023-06-01",
"x-stainless-lang" => "ruby",
"x-stainless-package-version" => STAINLESS_PACKAGE_VERSION,
"x-stainless-runtime" => "ruby",
"x-stainless-runtime-version" => RUBY_VERSION
)
end
# User-Agent (OAuth only — raw API-key callers don't set it)
headers["User-Agent"] = USER_AGENT if oauth
# Content-Type / Accept
headers["Content-Type"] = "application/json"
headers["Accept"] = if stream
"text/event-stream"
elsif claude_code_only
"application/json, text/plain, */*"
else
"application/json"
end
# Anthropic-Beta
betas = DEFAULT_BETAS.dup
betas << INTERLEAVED_THINKING_BETA if interleaved_thinking
betas.concat(Array(extra_betas))
betas = betas.uniq
headers["anthropic-beta"] = betas.join(",")
# Auth — highest priority, never overridable by caller extras
if oauth
headers["Authorization"] = "Bearer #{api_key}"
headers.delete("X-Api-Key")
else
headers["X-Api-Key"] = api_key.to_s
headers.delete("Authorization")
end
headers
end
end
end
end
end
|