summaryrefslogtreecommitdiffhomepage
path: root/lib/dispatch/adapter/claude/headers.rb
blob: 67c0394892491d3453dd0be6c978eb0741844cef (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
# 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