# frozen_string_literal: true module Dispatch module Adapter class Claude < Base module Cloaking CLAUDE_AGENT_INSTRUCTION = "You are a Claude agent, built on Anthropic's Claude Agent SDK." # Models that skip the Claude-Agent instruction block (per oh-my-pi). SKIP_AGENT_INSTRUCTION_PATTERN = /claude-3-5-haiku/i TOOL_PREFIX = "proxy_" BUILTINS = %w[web_search code_execution text_editor computer].freeze USER_ID_REGEX = /\A user_[0-9a-fA-F]{64} _account_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12} _session_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12} \z/x module_function # Generate the billing header string that gets prepended to the system # block array on OAuth requests. # # @param payload [Hash, nil] the full request params hash # @return [String] the billing header value def billing_header(payload) payload_json = JSON.generate(payload || {}) cch = Digest::SHA256.hexdigest(payload_json)[0, 5] build_hash = SecureRandom.hex(2)[0, 3] "x-anthropic-billing-header: cc_version=#{Headers::CLAUDE_CODE_VERSION}.#{build_hash}; cc_entrypoint=cli; cch=#{cch};" end # Prefix a tool name with "proxy_" unless it is a builtin or already prefixed. # # @param name [String] # @return [String] def apply_prefix(name) return name if BUILTINS.include?(name.downcase) return name if name.downcase.start_with?(TOOL_PREFIX) "#{TOOL_PREFIX}#{name}" end # Strip the "proxy_" prefix from a tool name (inverse of apply_prefix). # # @param name [String] # @return [String] def strip_prefix(name) return name unless name.downcase.start_with?(TOOL_PREFIX) name.sub(/\A#{TOOL_PREFIX}/i, "") end # Returns true if the string matches the cloaking user-id format. def cloaking_user_id?(str) str.is_a?(String) && USER_ID_REGEX.match?(str) end # Generate a fresh cloaking user id. def generate_cloaking_user_id user = SecureRandom.hex(32) account = SecureRandom.uuid.downcase session = SecureRandom.uuid.downcase "user_#{user}_account_#{account}_session_#{session}" end # Resolve the user_id for a request's metadata. # In OAuth mode, only pass through an already-cloaked id; otherwise generate one. # In API-key mode, pass through any String as-is; return nil for nil. def resolve_user_id(provided, is_oauth) return provided if provided.is_a?(String) && (!is_oauth || cloaking_user_id?(provided)) return nil unless is_oauth generate_cloaking_user_id end # Build the system-block array for the request body. # # @param user_system [String, Array, nil] # @param is_oauth [Boolean] # @param model_id [String] # @param cache_control [Hash, nil] attached to the last user block # @param billing_payload [Hash, nil] forwarded to billing_header # @return [Array, nil] def build_system_blocks(user_system, is_oauth:, model_id:, cache_control: nil, billing_payload: nil) # Normalise user_system into an Array user_blocks = normalise_system(user_system) # Short-circuit: pre-existing billing header → forward as-is return attach_cache_control(user_blocks, cache_control) if user_blocks.any? { |b| b["text"].to_s.start_with?("x-anthropic-billing-header:") } # Non-OAuth: no cloaking unless is_oauth return nil if user_blocks.empty? return attach_cache_control(user_blocks, cache_control) end # OAuth: inject billing + optional agent instruction prefix_blocks = [] billing_text = billing_header(billing_payload) prefix_blocks << text_block(billing_text) prefix_blocks << text_block(CLAUDE_AGENT_INSTRUCTION) unless model_id.to_s.match?(SKIP_AGENT_INSTRUCTION_PATTERN) all_blocks = prefix_blocks + user_blocks attach_cache_control(all_blocks, cache_control) end # Normalise a String, Array of TextBlock/Hash, or nil into Array def normalise_system(user_system) case user_system when nil [] when String user_system.empty? ? [] : [text_block(user_system)] when Array user_system.map do |block| if block.respond_to?(:to_h) h = block.to_h # Convert symbol keys to string keys h.transform_keys(&:to_s) else h = block.transform_keys(&:to_s) h end end else [text_block(user_system.to_s)] end end # Build a plain text block hash. def text_block(text) { "type" => "text", "text" => text } end # Attach cache_control to the last block (if provided). def attach_cache_control(blocks, cache_control) return blocks if cache_control.nil? || blocks.empty? result = blocks.map(&:dup) result.last["cache_control"] = cache_control result end end end end end