Class: Providers::Anthropic

Inherits:
Object
  • Object
show all
Includes:
HTTParty
Defined in:
lib/providers/anthropic.rb

Defined Under Namespace

Classes: ApiResponse, AuthenticationError, Error, RateLimitError, ServerError, TokenFormatError, TransientError

Constant Summary collapse

TOKEN_PREFIX =
"sk-ant-oat01-"
TOKEN_MIN_LENGTH =
80
API_VERSION =
"2023-06-01"
REQUIRED_BETA =
"oauth-2025-04-20"
OAUTH_PASSPHRASE =

Anthropic requires this exact string as the first system block for OAuth subscription tokens on Sonnet/Opus. Without it, /v1/messages returns 400.

"You are Claude Code, Anthropic's official CLI for Claude."
RATE_LIMIT_HEADERS =

Rate limit header names for extraction

{
  "5h_status" => "Anthropic-Ratelimit-Unified-5h-Status",
  "5h_reset" => "Anthropic-Ratelimit-Unified-5h-Reset",
  "5h_utilization" => "Anthropic-Ratelimit-Unified-5h-Utilization",
  "7d_status" => "Anthropic-Ratelimit-Unified-7d-Status",
  "7d_reset" => "Anthropic-Ratelimit-Unified-7d-Reset",
  "7d_utilization" => "Anthropic-Ratelimit-Unified-7d-Utilization"
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(token = nil) ⇒ Anthropic

Returns a new instance of Anthropic.



98
99
100
# File 'lib/providers/anthropic.rb', line 98

def initialize(token = nil)
  @token = token || self.class.fetch_token
end

Instance Attribute Details

#api_metricsHash? (readonly)

Returns rate limits and usage data.

Returns:

  • (Hash, nil)

    rate limits and usage data



37
38
39
40
41
42
43
44
45
46
# File 'lib/providers/anthropic.rb', line 37

ApiResponse = Data.define(:body, :api_metrics) do
  # Delegate Hash methods to body for backward compatibility.
  # Callers using response["content"] continue to work unchanged.
  def [](key) = body[key]
  def dig(...) = body.dig(...)
  def fetch(...) = body.fetch(...)
  def key?(key) = body.key?(key)
  def to_h = body
  def to_json(...) = body.to_json(...)
end

#bodyHash (readonly)

Returns parsed API response.

Returns:

  • (Hash)

    parsed API response



37
38
39
40
41
42
43
44
45
46
# File 'lib/providers/anthropic.rb', line 37

ApiResponse = Data.define(:body, :api_metrics) do
  # Delegate Hash methods to body for backward compatibility.
  # Callers using response["content"] continue to work unchanged.
  def [](key) = body[key]
  def dig(...) = body.dig(...)
  def fetch(...) = body.fetch(...)
  def key?(key) = body.key?(key)
  def to_h = body
  def to_json(...) = body.to_json(...)
end

#tokenObject (readonly)

Returns the value of attribute token.



96
97
98
# File 'lib/providers/anthropic.rb', line 96

def token
  @token
end

Class Method Details

.fetch_tokenObject



58
59
60
61
62
63
64
65
66
67
# File 'lib/providers/anthropic.rb', line 58

def fetch_token
  token = CredentialStore.read("anthropic", "subscription_token")
  return token if token.present?
  return "sk-ant-oat01-#{"0" * 68}" if ENV["CI"]

  raise AuthenticationError, <<~MSG.strip
    No Anthropic subscription token found in credentials.
    Use the TUI token setup (Ctrl+a → a) to configure your token.
  MSG
end

.validate_token_api!(token) ⇒ true

Validate a token against the live Anthropic API. Delegates to #validate_credentials! on a throwaway instance.

Parameters:

  • token (String)

    Anthropic API token to validate

Returns:

  • (true)

    when the API accepts the token

Raises:



90
91
92
93
# File 'lib/providers/anthropic.rb', line 90

def validate_token_api!(token)
  provider = new(token)
  provider.validate_credentials!
end

.validate_token_format!(token) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/providers/anthropic.rb', line 69

def validate_token_format!(token)
  unless token.start_with?(TOKEN_PREFIX)
    raise TokenFormatError,
      "Token must start with '#{TOKEN_PREFIX}'. Got: '#{token[0..12]}...'"
  end

  unless token.length >= TOKEN_MIN_LENGTH
    raise TokenFormatError,
      "Token must be at least #{TOKEN_MIN_LENGTH} characters (got #{token.length})"
  end

  true
end

Instance Method Details

#count_tokens(model:, messages:, **options) ⇒ Integer

Count tokens in a message payload without creating a message. Uses the free Anthropic token counting endpoint.

Parameters:

  • model (String)

    Anthropic model identifier

  • messages (Array<Hash>)

    conversation messages

  • options (Hash)

    additional parameters (e.g. system:, tools:)

Returns:

  • (Integer)

    estimated input token count

Raises:

  • (Error)

    on API errors



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/providers/anthropic.rb', line 140

def count_tokens(model:, messages:, **options)
  body = {model: model, messages: messages}.merge(options)

  response = self.class.post(
    "/v1/messages/count_tokens",
    body: body.to_json,
    headers: request_headers,
    timeout: Anima::Settings.api_timeout
  )

  result = handle_response(response)
  result["input_tokens"]
rescue Errno::ECONNRESET, Net::ReadTimeout, Net::OpenTimeout, SocketError, EOFError => network_error
  raise TransientError, "#{network_error.class}: #{network_error.message}"
end

#create_message(model:, messages:, max_tokens:, include_metrics: false, **options) ⇒ Hash, ApiResponse

Send a message to the Anthropic API and return the parsed response.

Parameters:

  • model (String)

    Anthropic model identifier

  • messages (Array<Hash>)

    conversation messages

  • max_tokens (Integer)

    maximum tokens in the response

  • include_metrics (Boolean) (defaults to: false)

    when true, returns an ApiResponse wrapper with both body and api_metrics; when false (default), returns just the parsed body Hash for backward compatibility

  • options (Hash)

    additional parameters (e.g. system:, tools:)

Returns:

  • (Hash, ApiResponse)

    parsed API response, or wrapper with metrics

Raises:



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/providers/anthropic.rb', line 115

def create_message(model:, messages:, max_tokens:, include_metrics: false, **options)
  wrap_system_prompt!(options)
  annotate_last_message_for_caching!(messages)
  body = {model: model, messages: messages, max_tokens: max_tokens}.merge(options)

  response = self.class.post(
    "/v1/messages",
    body: body.to_json,
    headers: request_headers,
    timeout: Anima::Settings.api_timeout
  )

  handle_response(response, include_metrics: include_metrics)
rescue Errno::ECONNRESET, Net::ReadTimeout, Net::OpenTimeout, SocketError, EOFError => network_error
  raise TransientError, "#{network_error.class}: #{network_error.message}"
end

#validate_credentials!true

Verify the token is accepted by Anthropic using the free models endpoint. Returns true on success; raises typed exceptions on failure so callers can distinguish permanent auth problems from transient outages.

Returns:

  • (true)

    when the API accepts the token

Raises:



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/providers/anthropic.rb', line 165

def validate_credentials!
  response = self.class.get(
    "/v1/models",
    headers: request_headers,
    timeout: Anima::Settings.api_timeout
  )

  case response.code
  when 200
    true
  when 401
    raise AuthenticationError,
      "Token rejected by Anthropic API (401). Re-run `claude setup-token` and use the TUI token setup (Ctrl+a → a)."
  when 403
    raise AuthenticationError,
      "Token not authorized for API access (403). This credential may be restricted to Claude Code only."
  else
    handle_response(response)
  end
rescue Errno::ECONNRESET, Net::ReadTimeout, Net::OpenTimeout, SocketError, EOFError => network_error
  raise TransientError, "#{network_error.class}: #{network_error.message}"
end