Class: Ask::Providers::Anthropic

Inherits:
Ask::Provider
  • Object
show all
Includes:
LLM::ProviderConfig, LLM::SSEBuffer
Defined in:
lib/ask/provider/anthropic.rb

Overview

Anthropic Claude API provider.

Class Method Summary collapse

Instance Method Summary collapse

Methods included from LLM::SSEBuffer

#each_sse_event, #init_sse_buffer

Constructor Details

#initialize(config = {}) ⇒ Anthropic

Returns a new instance of Anthropic.



10
11
12
13
14
# File 'lib/ask/provider/anthropic.rb', line 10

def initialize(config = {})
  config = normalize_config(config)
  super(config)
  @http = build_http
end

Class Method Details

.capabilitiesObject



57
58
59
60
61
62
# File 'lib/ask/provider/anthropic.rb', line 57

def capabilities
  {
    chat: true, streaming: true, tool_calls: true, vision: true,
    thinking: true, prompt_caching: true, structured_output: true
  }
end

.configuration_optionsObject



64
# File 'lib/ask/provider/anthropic.rb', line 64

def configuration_options; %i[api_key api_base]; end

.configuration_requirementsObject



65
# File 'lib/ask/provider/anthropic.rb', line 65

def configuration_requirements; %i[api_key]; end

.slugObject



55
# File 'lib/ask/provider/anthropic.rb', line 55

def slug; "anthropic"; end

Instance Method Details

#api_baseObject



16
17
18
# File 'lib/ask/provider/anthropic.rb', line 16

def api_base
  @config.api_base || "https://api.anthropic.com"
end

#build_request(messages, model:, tools: nil, temperature: nil, stream: nil, schema: nil, **params) ⇒ Object

--- Config transformation contract ---



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/ask/provider/anthropic.rb', line 70

def build_request(messages, model:, tools: nil, temperature: nil, stream: nil, schema: nil, **params)
  system_msgs, chat_msgs = messages.partition { |m| (m[:role] || m["role"]).to_s == "system" }
  system_content = format_system_content(system_msgs)

  payload = {
    model:,
    messages: chat_msgs.map { |m| format_message(m) },
    max_tokens: params.delete(:max_tokens) || 4096,
    stream: stream || false
  }

  payload[:system] = system_content if system_content
  tool_defs = format_tools(tools) if tools&.any?
  payload[:tools] = tool_defs if tool_defs
  payload[:temperature] = temperature if temperature
  payload.merge(params)
end

#chat(messages, model:, tools: nil, temperature: nil, stream: nil, schema: nil, **params, &block) ⇒ Object



28
29
30
31
32
33
34
35
36
# File 'lib/ask/provider/anthropic.rb', line 28

def chat(messages, model:, tools: nil, temperature: nil, stream: nil, schema: nil, **params, &block)
  msgs = messages.is_a?(Ask::Conversation) ? messages.to_a : messages
  payload = build_request(msgs, model:, tools:, temperature:, stream:, schema:, **params)
  if stream
    chat_stream(payload, model, &block)
  else
    chat_nonstream(payload, model)
  end
end

#embed(_texts, model: nil) ⇒ Object

Raises:

  • (Ask::CapabilityNotSupported)


38
39
40
# File 'lib/ask/provider/anthropic.rb', line 38

def embed(_texts, model: nil)
  raise Ask::CapabilityNotSupported, "Anthropic does not support embeddings"
end

#format_message(msg) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/ask/provider/anthropic.rb', line 148

def format_message(msg)
  role = (msg[:role] || msg["role"]).to_s
  content = msg[:content] || msg["content"]

  if msg[:tool_calls] || msg["tool_calls"]
    tc = msg[:tool_calls] || msg["tool_calls"]
    return {
      role:,
      content:,
      tool_calls: tc.map { |t|
        {
          type: "tool_use",
          id: t[:id] || t["id"],
          name: t.dig(:function, :name) || t.dig("function", "name") || t[:name],
          input: parse_json(t.dig(:function, :arguments) || t.dig("function", "arguments") || t[:arguments] || "{}")
        }
      }.compact
    }.compact
  end

  if msg[:tool_call_id] || msg["tool_call_id"]
    return {
      role: "user",
      content: [{
        type: "tool_result",
        tool_use_id: msg[:tool_call_id] || msg["tool_call_id"],
        content: content || ""
      }]
    }
  end

  { role:, content: }.compact
end

#format_tools(tools) ⇒ Object



138
139
140
141
142
143
144
145
146
# File 'lib/ask/provider/anthropic.rb', line 138

def format_tools(tools)
  tools.map do |t|
    {
      name: t.respond_to?(:name) ? t.name : t[:name],
      description: t.respond_to?(:description) ? t.description : t[:description],
      input_schema: t.respond_to?(:parameters) ? t.parameters : (t[:parameters] || { type: "object", properties: {} })
    }
  end
end

#headersObject



20
21
22
23
24
25
26
# File 'lib/ask/provider/anthropic.rb', line 20

def headers
  {
    "x-api-key" => @config.api_key,
    "anthropic-version" => "2023-06-01",
    "Content-Type" => "application/json"
  }
end

#list_modelsObject



42
43
44
45
46
47
# File 'lib/ask/provider/anthropic.rb', line 42

def list_models
  response = @http.get("v1/models")
  return [] unless response.success?

  response.body["data"].map { |m| Ask::ModelInfo.new(id: m["id"], provider: slug) }
end

#parse_error(response) ⇒ Object



49
50
51
52
# File 'lib/ask/provider/anthropic.rb', line 49

def parse_error(response)
  body = response.body rescue nil
  body&.dig("error", "message") || body&.dig("error", "type")
end

#parse_response(body, model) ⇒ Object



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/ask/provider/anthropic.rb', line 88

def parse_response(body, model)
  content_blocks = body["content"] || []
  text_content = content_blocks.select { |c| c["type"] == "text" }.map { |c| c["text"] }.join
  tool_blocks = content_blocks.select { |c| c["type"] == "tool_use" }
  thinking_blocks = content_blocks.select { |c| %w[thinking redacted_thinking].include?(c["type"]) }
  usage = body["usage"] || {}

  tool_calls = tool_blocks.map do |tb|
    { id: tb["id"], type: "function", name: tb["name"], arguments: JSON.generate(tb["input"]) }
  end

   = {
    model: body["model"] || model,
    stop_reason: body["stop_reason"],
    stop_sequence: body["stop_sequence"],
    input_tokens: usage["input_tokens"],
    output_tokens: usage["output_tokens"],
    thinking: thinking_blocks.map { |b| b["thinking"] || b["text"] }.compact.join("\n"),
    raw: body
  }.compact

  text = text_content.empty? ? nil : text_content
  Ask::Message.new(role: :assistant, content: text, tool_calls: tool_calls.empty? ? nil : tool_calls, metadata:)
end

#parse_stream(raw, stream, model, &block) ⇒ Object



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/ask/provider/anthropic.rb', line 113

def parse_stream(raw, stream, model, &block)
  each_sse_event(raw) do |data|
    parsed = JSON.parse(data) rescue next

    case parsed["type"]
    when "content_block_delta"
      delta = parsed.dig("delta")
      next unless delta

      chunk = Ask::Chunk.new(content: delta["text"])
      stream.add(chunk)
      yield chunk if block_given?
    when "message_stop"
      usage = parsed["usage"] || parsed["message"]&.dig("usage")
      if usage
        chunk = Ask::Chunk.new(finish_reason: "stop", usage:)
        stream.add(chunk)
        yield chunk if block_given?
      end
    when "message_start"
      # Handled by content_block_start instead
    end
  end
end