Class: RubynCode::LLM::Adapters::Anthropic

Inherits:
Base
  • Object
show all
Includes:
JsonParsing, PromptCaching
Defined in:
lib/rubyn_code/llm/adapters/anthropic.rb

Overview

rubocop:disable Metrics/ClassLength -- auth helpers + streaming + finalize are all needed

Direct Known Subclasses

AnthropicCompatible

Constant Summary collapse

API_URL =
'https://api.anthropic.com/v1/messages'
ANTHROPIC_VERSION =
'2023-06-01'
MAX_RETRIES =
3
RETRY_DELAYS =
[2, 5, 10].freeze
FALLBACK_ELIGIBLE_MODELS =

Fable/Mythos/Opus 5 models opt into a server-side Opus 4.8 fallback by default: a declined request (safety-classifier refusal) is transparently re-served by the fallback model in the same call. No config toggle — hardcoded for these model families only.

/\Aclaude-(fable|mythos|opus-5)/
FALLBACK_BETA =
'server-side-fallback-2026-06-01'
TASK_BUDGET_BETA =
'task-budgets-2026-03-13'
TASK_BUDGET_MIN_TOKENS =

API minimum; below this, task_budget is omitted entirely

20_000
TASK_BUDGET_MODELS =
/\Aclaude-(fable|mythos|sonnet-5|opus-5|opus-4-[78])/
AVAILABLE_MODELS =
%w[
  claude-fable-5
  claude-opus-5
  claude-opus-4-8
  claude-opus-4-7
  claude-opus-4-6
  claude-sonnet-5
  claude-sonnet-4-6
  claude-haiku-4-5
].freeze
ADAPTIVE_THINKING_MODELS =

Models on the adaptive-thinking API surface (Claude 4.6+). budget_tokens is removed there and returns a 400; older models (Haiku 4.5, Sonnet/Opus 4.5 and earlier) still take enabled + budget.

/\Aclaude-(fable|mythos|opus-5|opus-4-[678]|sonnet-5|sonnet-4-6)/

Constants included from PromptCaching

PromptCaching::CACHE_EPHEMERAL, PromptCaching::OAUTH_GATE

Instance Method Summary collapse

Instance Method Details

#access_tokenString?

Returns the access token in use. Accepts either a raw String token or a Hash like { access_token: "...", expires_at: ..., source: :keychain }. Memoized per-instance so that repeated calls during one request don't re-hit the token store.

Returns:

  • (String, nil)

    the access token in use. Accepts either a raw String token or a Hash like { access_token: "...", expires_at: ..., source: :keychain }. Memoized per-instance so that repeated calls during one request don't re-hit the token store.



102
103
104
105
106
107
108
109
110
111
112
# File 'lib/rubyn_code/llm/adapters/anthropic.rb', line 102

def access_token
  return @access_token if defined?(@access_token)

  token = raw_access_token
  @access_token =
    case token
    when nil    then nil
    when String then token.empty? ? nil : token
    when Hash   then token[:access_token] || token['access_token']
    end
end

#api_keyString?

Returns the API key (ANTHROPIC_API_KEY), or nil.

Returns:

  • (String, nil)

    the API key (ANTHROPIC_API_KEY), or nil



115
116
117
# File 'lib/rubyn_code/llm/adapters/anthropic.rb', line 115

def api_key
  ENV.fetch('ANTHROPIC_API_KEY', nil)
end

#chat(messages:, model:, max_tokens:, tools: nil, system: nil, on_text: nil, task_budget: nil, thinking: nil, effort: nil) ⇒ Object

rubocop:disable Metrics/ParameterLists -- mirrors LLM adapter interface



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/rubyn_code/llm/adapters/anthropic.rb', line 55

def chat(messages:, model:, max_tokens:, tools: nil, system: nil, on_text: nil, # rubocop:disable Metrics/ParameterLists -- mirrors LLM adapter interface
         task_budget: nil, thinking: nil, effort: nil)
  ensure_valid_token!
  use_streaming = on_text && oauth_token?

  body = build_request_body(
    messages: messages, tools: tools, system: system,
    model: model, max_tokens: max_tokens,
    stream: use_streaming, task_budget: task_budget,
    thinking: thinking, effort: effort
  )

  return stream_request(body, on_text) if use_streaming

  execute_with_retries(body, on_text)
end

#ensure_valid_token!Object

Validate the active auth path before sending the request. Raises Client::AuthExpiredError with a clear message instead of letting the HTTP request fail with a 401.



77
78
79
80
81
82
83
# File 'lib/rubyn_code/llm/adapters/anthropic.rb', line 77

def ensure_valid_token!
  return if token_valid? && access_token && !access_token.empty?

  return if !oauth_token? && api_key && !api_key.empty?

  raise Client::AuthExpiredError, 'No valid authentication configured'
end

#modelsObject



51
52
53
# File 'lib/rubyn_code/llm/adapters/anthropic.rb', line 51

def models
  AVAILABLE_MODELS
end

#oauth_token?Boolean

Returns true when using the OAuth token (vs API key). A token from the keychain (or marked source: :keychain / :oauth) is treated as OAuth; a token from :env or :tokens_file is treated as a long-lived API key.

Returns:

  • (Boolean)

    true when using the OAuth token (vs API key). A token from the keychain (or marked source: :keychain / :oauth) is treated as OAuth; a token from :env or :tokens_file is treated as a long-lived API key.



89
90
91
92
93
94
95
# File 'lib/rubyn_code/llm/adapters/anthropic.rb', line 89

def oauth_token?
  token = raw_access_token
  return false unless token.is_a?(Hash)

  source = token[:source] || token['source']
  [:keychain, 'keychain', :oauth].include?(source)
end

#provider_nameObject



47
48
49
# File 'lib/rubyn_code/llm/adapters/anthropic.rb', line 47

def provider_name
  'anthropic'
end

#raw_access_tokenObject



128
129
130
131
132
133
# File 'lib/rubyn_code/llm/adapters/anthropic.rb', line 128

def raw_access_token
  return @raw_access_token if defined?(@raw_access_token)

  @raw_access_token =
    (Auth::TokenStore.load if defined?(Auth::TokenStore) && Auth::TokenStore.respond_to?(:load))
end

#token_valid?Boolean

Returns true if the token store says the active credential is still valid.

Returns:

  • (Boolean)

    true if the token store says the active credential is still valid



121
122
123
124
125
126
# File 'lib/rubyn_code/llm/adapters/anthropic.rb', line 121

def token_valid?
  return true unless defined?(Auth::TokenStore)
  return true unless Auth::TokenStore.respond_to?(:valid?)

  Auth::TokenStore.valid?
end