# 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] 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] 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