Module: RubyLLM::Providers::Anthropic::Chat

Included in:
RubyLLM::Providers::Anthropic
Defined in:
lib/ruby_llm/providers/anthropic/chat.rb

Overview

Chat methods for the Anthropic API implementation

Class Method Summary collapse

Class Method Details

.adaptive_thinking_payload(effort) ⇒ Object



271
272
273
274
275
276
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 271

def adaptive_thinking_payload(effort)
  {
    thinking: { type: 'adaptive' },
    output_config: { effort: effort }
  }
end

.add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:, schema: nil) ⇒ Object

rubocop:disable Metrics/ParameterLists



61
62
63
64
65
66
67
68
69
70
71
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 61

def add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:, schema: nil) # rubocop:disable Metrics/ParameterLists
  if tools.any?
    payload[:tools] = tools.values.map { |t| Tools.function_for(t) }
    unless tool_prefs[:choice].nil? && tool_prefs[:calls].nil?
      payload[:tool_choice] = Tools.build_tool_choice(tool_prefs)
    end
  end
  payload[:system] = system_content unless system_content.empty?
  payload[:temperature] = temperature unless temperature.nil?
  payload[:output_config] = payload.fetch(:output_config, {}).merge(build_output_config(schema)) if schema
end

.add_thinking_fields(payload, thinking, model) ⇒ Object



233
234
235
236
237
238
239
240
241
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 233

def add_thinking_fields(payload, thinking, model)
  thinking_payload = build_thinking_payload(thinking, model)
  return unless thinking_payload

  payload[:thinking] = thinking_payload[:thinking] if thinking_payload[:thinking]
  return unless thinking_payload[:output_config]

  payload[:output_config] = payload.fetch(:output_config, {}).merge(thinking_payload[:output_config])
end

.append_formatted_content(content_blocks, content) ⇒ Object



217
218
219
220
221
222
223
224
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 217

def append_formatted_content(content_blocks, content)
  formatted_content = Media.format_content(content)
  if formatted_content.is_a?(Array)
    content_blocks.concat(formatted_content)
  else
    content_blocks << formatted_content
  end
end

.build_base_payload(chat_messages, model, stream, thinking) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 48

def build_base_payload(chat_messages, model, stream, thinking)
  payload = {
    model: model.id,
    messages: chat_messages.map { |msg| format_message(msg, thinking: thinking) },
    stream: stream,
    max_tokens: model.max_tokens || 4096
  }

  add_thinking_fields(payload, thinking, model)

  payload
end

.build_message(data, content, thinking, thinking_signature, tool_use_blocks, response) ⇒ Object

rubocop:disable Metrics/ParameterLists



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
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 109

def build_message(data, content, thinking, thinking_signature, tool_use_blocks, response) # rubocop:disable Metrics/ParameterLists
  usage = data['usage'] || {}
  cached_tokens = usage['cache_read_input_tokens']
  cache_creation_tokens = usage['cache_creation_input_tokens']
  if cache_creation_tokens.nil? && usage['cache_creation'].is_a?(Hash)
    cache_creation_tokens = usage['cache_creation'].values.compact.sum
  end
  thinking_tokens = usage.dig('output_tokens_details', 'thinking_tokens') ||
                    usage.dig('output_tokens_details', 'reasoning_tokens') ||
                    usage['thinking_tokens'] ||
                    usage['reasoning_tokens']

  Message.new(
    role: :assistant,
    content: content,
    thinking: Thinking.build(text: thinking, signature: thinking_signature),
    tool_calls: Tools.parse_tool_calls(tool_use_blocks),
    input_tokens: usage['input_tokens'],
    output_tokens: usage['output_tokens'],
    cached_tokens: cached_tokens,
    cache_creation_tokens: cache_creation_tokens,
    thinking_tokens: thinking_tokens,
    model_id: data['model'],
    raw: response
  )
end

.build_output_config(schema) ⇒ Object



73
74
75
76
77
78
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 73

def build_output_config(schema)
  normalized = RubyLLM::Utils.deep_dup(schema[:schema])
  normalized.delete(:strict)
  normalized.delete('strict')
  { format: { type: 'json_schema', schema: normalized } }
end

.build_system_content(system_messages) ⇒ Object



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 31

def build_system_content(system_messages)
  return [] if system_messages.empty?

  # Anthropic's `system` parameter accepts an array of text content blocks
  # (each optionally with cache_control); each :system message becomes its
  # own block in the resulting array.
  system_messages.flat_map do |msg|
    content = msg.content

    if content.is_a?(RubyLLM::Content::Raw)
      content.value
    else
      Media.format_content(content)
    end
  end
end

.build_thinking_block(thinking) ⇒ Object



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 200

def build_thinking_block(thinking)
  return nil unless thinking

  if thinking.text
    {
      type: 'thinking',
      thinking: thinking.text,
      signature: thinking.signature
    }.compact
  elsif thinking.signature
    {
      type: 'redacted_thinking',
      data: thinking.signature
    }
  end
end

.build_thinking_payload(thinking, model) ⇒ Object

Raises:

  • (ArgumentError)


243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 243

def build_thinking_payload(thinking, model)
  return nil unless thinking&.enabled?

  effort = resolve_effort(thinking)
  return nil if effort == 'none'

  budget = resolve_budget(thinking)
  if budget
    return enabled_thinking_payload(budget) if model.reasoning_option('budget_tokens')

    raise ArgumentError, "Anthropic thinking budget is not supported for #{model.id}"
  end

  raise ArgumentError, 'Anthropic adaptive thinking requires an effort' if effort.nil?
  return adaptive_thinking_payload(effort) if model.reasoning_option('effort')

  raise ArgumentError, "Anthropic thinking effort is not supported for #{model.id}"
end

.completion_urlObject



10
11
12
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 10

def completion_url
  'v1/messages'
end

.convert_role(role) ⇒ Object



226
227
228
229
230
231
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 226

def convert_role(role)
  case role
  when :tool, :user then 'user'
  else 'assistant'
  end
end

.enabled_thinking_payload(budget) ⇒ Object



262
263
264
265
266
267
268
269
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 262

def enabled_thinking_payload(budget)
  {
    thinking: {
      type: 'enabled',
      budget_tokens: budget
    }
  }
end

.extract_text_content(blocks) ⇒ Object



92
93
94
95
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 92

def extract_text_content(blocks)
  text_blocks = blocks.select { |c| c['type'] == 'text' }
  text_blocks.map { |c| c['text'] }.join
end

.extract_thinking_content(blocks) ⇒ Object



97
98
99
100
101
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 97

def extract_thinking_content(blocks)
  thinking_blocks = blocks.select { |c| c['type'] == 'thinking' }
  thoughts = thinking_blocks.map { |c| c['thinking'] || c['text'] }.join
  thoughts.empty? ? nil : thoughts
end

.extract_thinking_signature(blocks) ⇒ Object



103
104
105
106
107
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 103

def extract_thinking_signature(blocks)
  thinking_block = blocks.find { |c| c['type'] == 'thinking' } ||
                   blocks.find { |c| c['type'] == 'redacted_thinking' }
  thinking_block&.dig('signature') || thinking_block&.dig('data')
end

.format_basic_message_with_thinking(msg, thinking_enabled) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 148

def format_basic_message_with_thinking(msg, thinking_enabled)
  content_blocks = []

  if msg.role == :assistant && thinking_enabled
    thinking_block = build_thinking_block(msg.thinking)
    content_blocks << thinking_block if thinking_block
  end

  append_formatted_content(content_blocks, msg.content)

  {
    role: convert_role(msg.role),
    content: content_blocks
  }
end

.format_message(msg, thinking: nil) ⇒ Object



136
137
138
139
140
141
142
143
144
145
146
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 136

def format_message(msg, thinking: nil)
  thinking_enabled = thinking&.enabled?

  if msg.tool_call?
    format_tool_call_with_thinking(msg, thinking_enabled)
  elsif msg.tool_result?
    Tools.format_tool_result(msg)
  else
    format_basic_message_with_thinking(msg, thinking_enabled)
  end
end

.format_tool_call_with_thinking(msg, thinking_enabled) ⇒ Object



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

def format_tool_call_with_thinking(msg, thinking_enabled)
  if msg.content.is_a?(RubyLLM::Content::Raw)
    content_blocks = msg.content.value
    content_blocks = [content_blocks] unless content_blocks.is_a?(Array)
    content_blocks = prepend_thinking_block(content_blocks, msg, thinking_enabled)

    return { role: 'assistant', content: content_blocks }
  end

  content_blocks = prepend_thinking_block([], msg, thinking_enabled)
  append_formatted_content(content_blocks, msg.content) unless msg.content.nil? || msg.content.empty?

  msg.tool_calls.each_value do |tool_call|
    content_blocks << {
      type: 'tool_use',
      id: tool_call.id,
      name: tool_call.name,
      input: tool_call.arguments
    }
  end

  {
    role: 'assistant',
    content: content_blocks
  }
end

.parse_completion_response(response) ⇒ Object



80
81
82
83
84
85
86
87
88
89
90
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 80

def parse_completion_response(response)
  data = response.body
  content_blocks = data['content'] || []

  text_content = extract_text_content(content_blocks)
  thinking_content = extract_thinking_content(content_blocks)
  thinking_signature = extract_thinking_signature(content_blocks)
  tool_use_blocks = Tools.find_tool_uses(content_blocks)

  build_message(data, text_content, thinking_content, thinking_signature, tool_use_blocks, response)
end

.prepend_thinking_block(content_blocks, msg, thinking_enabled) ⇒ Object



191
192
193
194
195
196
197
198
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 191

def prepend_thinking_block(content_blocks, msg, thinking_enabled)
  return content_blocks unless thinking_enabled

  thinking_block = build_thinking_block(msg.thinking)
  content_blocks.unshift(thinking_block) if thinking_block

  content_blocks
end

.render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil, tool_prefs: nil) ⇒ Object

rubocop:disable Metrics/ParameterLists



15
16
17
18
19
20
21
22
23
24
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 15

def render_payload(messages, tools:, temperature:, model:, stream: false,
                   schema: nil, thinking: nil, tool_prefs: nil)
  tool_prefs ||= {}
  system_messages, chat_messages = separate_messages(messages)
  system_content = build_system_content(system_messages)

  build_base_payload(chat_messages, model, stream, thinking).tap do |payload|
    add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:, schema:)
  end
end

.resolve_budget(thinking) ⇒ Object



284
285
286
287
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 284

def resolve_budget(thinking)
  budget = thinking.respond_to?(:budget) ? thinking.budget : thinking
  budget.is_a?(Integer) ? budget : nil
end

.resolve_effort(thinking) ⇒ Object



278
279
280
281
282
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 278

def resolve_effort(thinking)
  effort = thinking.respond_to?(:effort) ? thinking.effort : nil
  effort = effort.to_s if effort
  effort.nil? || effort.empty? ? nil : effort
end

.separate_messages(messages) ⇒ Object

rubocop:enable Metrics/ParameterLists



27
28
29
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 27

def separate_messages(messages)
  messages.partition { |msg| msg.role == :system }
end