summaryrefslogtreecommitdiffhomepage
path: root/lib/dispatch/adapter/claude/cloaking.rb
blob: fc50ccae89492e2f2dc90859531a6c77a01259f8 (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
# 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<Hash>, 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<Hash>, nil]
        def build_system_blocks(user_system, is_oauth:, model_id:, cache_control: nil, billing_payload: nil)
          # Normalise user_system into an Array<Hash>
          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<Hash>
        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