Module: RubyLLM::Protocols::Anthropic::Chat

Defined in:
lib/ruby_llm/protocols/anthropic/chat.rb

Overview

Chat methods for the Anthropic API implementation

Constant Summary collapse

FINISH_REASONS =
{
  'end_turn' => :stop, 'stop_sequence' => :stop, 'max_tokens' => :max_tokens,
  'model_context_window_exceeded' => :max_tokens, 'tool_use' => :tool_calls, 'refusal' => :content_filter
}.freeze
ANTHROPIC_INLINE_REQUEST_LIMIT =
24 * 1024 * 1024
ANTHROPIC_FILE_UPLOAD_LIMIT =
500 * 1024 * 1024
CACHE_CONTROL_TYPE =
'ephemeral'
PROMPT_CACHE_OPTIONS =
%i[ttl].freeze
BETA_HEADER =
'anthropic-beta'
COMPACTION_BETA =
'compact-2026-01-12'
COMPACTION_EDIT_TYPE =
'compact_20260112'
COUNT_TOKENS_KEYS =
%i[model messages system tools tool_choice thinking].freeze
THINKING_BLOCK_TYPES =
%w[thinking redacted_thinking].freeze
EFFORT_BUDGETS =
{ 'low' => 1024, 'medium' => 40_000, 'high' => 63_999 }.freeze

Class Method Summary collapse

Class Method Details

.adaptive_thinking?(thinking, effort, model) ⇒ Boolean

Returns:

  • (Boolean)


519
520
521
522
523
524
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 519

def adaptive_thinking?(thinking, effort, model)
  return true if thinking.display
  return false unless effort

  model.reasoning_option(:effort) && !model.reasoning_option(:budget_tokens)
end

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



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

def add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:, schema: nil)
  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



476
477
478
479
480
481
482
483
484
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 476

def add_thinking_fields(payload, thinking, model)
  thinking_payload = build_thinking_payload(thinking, model, payload[:max_tokens])
  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, msg, citations: false) ⇒ Object



427
428
429
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 427

def append_formatted_content(content_blocks, msg, citations: false)
  content_blocks.concat(Media.format_content(msg.content, msg.attachments, citations: citations))
end

.apply_compaction(payload, compaction) ⇒ Object

Anthropic compacts through a context_management edit. Omitting the trigger leaves the API on its own default threshold; the API rejects an explicit one below its minimum, so RubyLLM passes the value through rather than second-guessing it.



155
156
157
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 155

def apply_compaction(payload, compaction)
  Support::Utils.deep_merge(payload, { context_management: { edits: [compaction_edit(compaction)] } })
end

.apply_compaction_headers(headers, _compaction) ⇒ Object



167
168
169
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 167

def apply_compaction_headers(headers, _compaction)
  headers.merge(BETA_HEADER => join_betas(headers[BETA_HEADER], COMPACTION_BETA))
end

.apply_end_user(payload, identifier) ⇒ Object



147
148
149
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 147

def apply_end_user(payload, identifier)
  Support::Utils.deep_merge(payload, { metadata: { user_id: identifier } })
end

.apply_files_beta(headers, payload) ⇒ Object

The Files API is still a beta, so a request that references an uploaded file has to carry its beta header too.



173
174
175
176
177
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 173

def apply_files_beta(headers, payload)
  return headers unless provider_file_source?(payload)

  headers.merge(BETA_HEADER => join_betas(headers[BETA_HEADER], Files::BETA_HEADER))
end

.build_base_payload(chat_messages, model, stream, thinking, citations: false, caching: nil, max_output_tokens: nil) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 97

def build_base_payload(chat_messages, model, stream, thinking, citations: false, caching: nil,
                       max_output_tokens: nil)
  payload = {
    model: model.id,
    messages: format_messages(chat_messages, thinking:, citations:, caching:),
    stream: stream,
    max_tokens: max_output_tokens || model.max_output_tokens || 4096
  }

  add_thinking_fields(payload, thinking, model)

  payload
end

.build_message(data, content:, citations:, thinking:, thinking_signature:, tool_use_blocks:, raw:, server_tool_calls: [], raw_content: nil) ⇒ Object



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 311

def build_message(data, content:, citations:, thinking:, thinking_signature:, tool_use_blocks:, raw:,
                  server_tool_calls: [], raw_content: nil)
  usage = aggregate_usage(data['usage'])
  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,
    citations: citations,
    thinking: Thinking.build(text: thinking, signature: thinking_signature),
    raw_reasoning: parse_thinking_blocks(data['content'] || []),
    tool_calls: Tools.parse_tool_calls(tool_use_blocks),
    server_tool_calls: server_tool_calls,
    raw_content: raw_content,
    input_tokens: usage['input_tokens'],
    output_tokens: usage['output_tokens'],
    cache_read_tokens: extract_cache_read_tokens(data),
    cache_write_tokens: extract_cache_write_tokens(data),
    thinking_tokens: thinking_tokens,
    server_tool_use: usage['server_tool_use'],
    finish_reason: normalize_finish_reason(data['stop_reason']),
    model: data['model'],
    raw: raw
  )
end

.build_output_config(schema) ⇒ Object



207
208
209
210
211
212
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 207

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

.build_system_content(system_messages, caching: nil) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 85

def build_system_content(system_messages, caching: nil)
  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|
    blocks = Media.format_content(msg.content, msg.attachments).dup
    cache_boundary?(msg, caching:) ? inject_cache_control(blocks, caching:) : blocks
  end
end

.build_thinking_block(thinking) ⇒ Object



410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 410

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, max_tokens) ⇒ Object



486
487
488
489
490
491
492
493
494
495
496
497
498
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 486

def build_thinking_payload(thinking, model, max_tokens)
  return nil unless thinking&.enabled?
  return { thinking: { type: 'disabled' } } if thinking.enabled == false

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

  payload = {}
  mode = thinking_mode(thinking, model, effort, max_tokens)
  payload[:thinking] = mode if mode
  payload[:output_config] = { effort: effort } if effort
  payload
end

.cache_boundary?(message, caching: nil) ⇒ Boolean

Returns:

  • (Boolean)


431
432
433
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 431

def cache_boundary?(message, caching: nil)
  caching != false && message.cache_until_here?
end

.citation_url(data) ⇒ Object

Search result citations carry the developer-provided source string.



286
287
288
289
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 286

def citation_url(data)
  url = data['url'] || data['source']
  url if url&.match?(%r{\Ahttps?://}i)
end

.compaction_edit(compaction) ⇒ Object



159
160
161
162
163
164
165
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 159

def compaction_edit(compaction)
  edit = { type: COMPACTION_EDIT_TYPE }
  edit[:trigger] = { type: 'input_tokens', value: compaction[:at] } if compaction[:at]
  edit[:instructions] = compaction[:instructions] if compaction[:instructions]
  edit[:pause_after_compaction] = true if compaction[:pause_after]
  edit
end

.completion_urlObject



33
34
35
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 33

def completion_url
  'v1/messages'
end

.convert_role(role) ⇒ Object



469
470
471
472
473
474
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 469

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

.count_tokens_urlObject



51
52
53
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 51

def count_tokens_url
  'v1/messages/count_tokens'
end

.default_large_file_upload_thresholdObject



195
196
197
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 195

def default_large_file_upload_threshold
  ANTHROPIC_INLINE_REQUEST_LIMIT
end

.effort_budget(effort, model, max_tokens) ⇒ Object



526
527
528
529
530
531
532
533
534
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 526

def effort_budget(effort, model, max_tokens)
  return nil unless effort && model.reasoning_option(:budget_tokens)

  budget = EFFORT_BUDGETS.fetch(effort, EFFORT_BUDGETS['high'])
  minimum = [model.reasoning_option(:budget_tokens)[:min].to_i, 1].max
  return [budget, minimum].max unless max_tokens

  budget.clamp(minimum, [max_tokens - 1, minimum].max)
end

.extract_server_tool_calls(blocks) ⇒ Object



237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 237

def extract_server_tool_calls(blocks)
  blocks.select { |block| server_tool_block?(block) }.map do |block|
    ServerToolCall.new(
      type: block['type'],
      name: block['name'],
      id: block['id'] || block['tool_use_id'],
      input: block['input'],
      result: block['content'],
      raw: block
    )
  end
end

.extract_text_and_citations(blocks) ⇒ Object



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 250

def extract_text_and_citations(blocks)
  text = +''
  citations = []

  blocks.each do |block|
    next unless block['type'] == 'text'

    block_text = block['text'].to_s
    Array(block['citations']).each do |citation|
      citations << parse_citation(citation, text: block_text,
                                            start_index: text.length,
                                            end_index: text.length + block_text.length)
    end
    text << block_text
  end

  [text, citations]
end

.extract_thinking_content(blocks) ⇒ Object

An empty thinking text is a real block whose display was omitted; collapsing it to nil would replay the signature as redacted data.



293
294
295
296
297
298
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 293

def extract_thinking_content(blocks)
  thinking_blocks = blocks.select { |c| c['type'] == 'thinking' }
  return nil if thinking_blocks.empty?

  thinking_blocks.map { |c| c['thinking'] || c['text'] }.join
end

.extract_thinking_signature(blocks) ⇒ Object



300
301
302
303
304
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 300

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

.finish_reasonsObject



25
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 25

def finish_reasons = FINISH_REASONS

.format_basic_message_with_thinking(msg, citations: false, caching: nil) ⇒ Object



365
366
367
368
369
370
371
372
373
374
375
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 365

def format_basic_message_with_thinking(msg, citations: false, caching: nil)
  content_blocks = msg.role == :assistant ? format_thinking_blocks(msg) : []

  append_formatted_content(content_blocks, msg, citations: citations)
  inject_cache_control(content_blocks, caching:) if cache_boundary?(msg, caching:)

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

.format_cache_option_keys(keys) ⇒ Object



465
466
467
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 465

def format_cache_option_keys(keys)
  keys.map { |key| ":#{key}" }.join(', ')
end

.format_message(msg, thinking: nil, citations: false, caching: nil) ⇒ Object

Stored thinking blocks replay whether or not this request asks for thinking: Claude emits them on its own and requires them back on tool-use turns.



343
344
345
346
347
348
349
350
351
352
353
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 343

def format_message(msg, thinking: nil, citations: false, caching: nil) # rubocop:disable Lint/UnusedMethodArgument
  if msg.role == :assistant && msg.raw_content
    format_raw_assistant_message(msg, caching:)
  elsif msg.tool_call?
    format_tool_call_with_thinking(msg, caching:)
  elsif msg.tool_result?
    Tools.format_tool_result(msg)
  else
    format_basic_message_with_thinking(msg, citations: citations, caching:)
  end
end

.format_messages(messages, thinking: nil, citations: false, caching: nil) ⇒ Object



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 111

def format_messages(messages, thinking: nil, citations: false, caching: nil)
  rendered = []
  tool_result_blocks = []

  messages.each do |msg|
    if msg.tool_result?
      tool_result_blocks << Tools.format_tool_result_block(msg)
      inject_cache_control(tool_result_blocks, caching:) if cache_boundary?(msg, caching:)
      next
    end

    unless tool_result_blocks.empty?
      rendered << { role: 'user', content: tool_result_blocks }
      tool_result_blocks = []
    end

    formatted = format_message(msg, thinking:, citations:, caching:)
    rendered << formatted unless formatted[:content].empty?
  end

  rendered << { role: 'user', content: tool_result_blocks } unless tool_result_blocks.empty?
  rendered
end

.format_raw_assistant_message(msg, caching: nil) ⇒ Object

Turns that used server tools replay their provider-shaped blocks verbatim: the API requires the tool_use/result blocks and their citations back exactly as returned.



358
359
360
361
362
363
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 358

def format_raw_assistant_message(msg, caching: nil)
  blocks = msg.raw_content.dup
  inject_cache_control(blocks, caching:) if cache_boundary?(msg, caching:)

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

.format_thinking_blocks(msg) ⇒ Object



403
404
405
406
407
408
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 403

def format_thinking_blocks(msg)
  blocks = msg.raw_reasoning['anthropic'] if msg.raw_reasoning.is_a?(Hash)
  return Support::Utils.deep_dup(blocks) if blocks

  [build_thinking_block(msg.thinking)].compact
end

.format_tool_call_with_thinking(msg, caching: nil) ⇒ Object



377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 377

def format_tool_call_with_thinking(msg, caching: nil)
  content_blocks = prepend_thinking_blocks([], msg)
  append_formatted_content(content_blocks, msg) 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
  inject_cache_control(content_blocks, caching:) if cache_boundary?(msg, caching:)

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

.inject_cache_control(blocks, caching: nil) ⇒ Object



435
436
437
438
439
440
441
442
443
444
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 435

def inject_cache_control(blocks, caching: nil)
  return blocks if blocks.empty?

  last = blocks.last
  return blocks if last.is_a?(Hash) && (last[:cache_control] || last['cache_control'])
  return blocks unless last.is_a?(Hash)

  blocks[-1] = last.merge(cache_control: prompt_cache_control(caching))
  blocks
end

.join_betas(existing, beta) ⇒ Object

Anthropic takes several betas as one comma-separated header, so an added beta joins whatever a server tool or with_headers already set.



187
188
189
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 187

def join_betas(existing, beta)
  (existing.to_s.split(',').map(&:strip).reject(&:empty?) + [beta]).uniq.join(',')
end

.normalize_finish_reason(reason) ⇒ Object



27
28
29
30
31
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 27

def normalize_finish_reason(reason)
  return nil if reason.nil?

  finish_reasons.fetch(reason.to_s) { reason.to_s.to_sym }
end

.parse_citation(data, text: nil, start_index: nil, end_index: nil) ⇒ Object



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 269

def parse_citation(data, text: nil, start_index: nil, end_index: nil)
  end_page = data['end_page_number']

  Citation.new(
    url: citation_url(data),
    title: data['document_title'] || data['title'],
    cited_text: data['cited_text'],
    text: text,
    start_index: start_index,
    end_index: end_index,
    source_index: data['document_index'] || data['search_result_index'],
    start_page: data['start_page_number'],
    end_page: end_page && (end_page - 1)
  )
end

.parse_completion_body(data, raw:) ⇒ Object



214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 214

def parse_completion_body(data, raw:)
  content_blocks = data['content'] || []

  text_content, citations = extract_text_and_citations(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)
  server_tool_calls = extract_server_tool_calls(content_blocks)

  build_message(data, content: text_content, citations:, thinking: thinking_content,
                      thinking_signature:, tool_use_blocks:, server_tool_calls:,
                      raw_content: server_tool_calls.any? ? content_blocks : nil, raw:)
end

.parse_count_tokens_response(response) ⇒ Object



70
71
72
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 70

def parse_count_tokens_response(response)
  response.body['input_tokens']
end

.parse_thinking_blocks(blocks) ⇒ Object



306
307
308
309
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 306

def parse_thinking_blocks(blocks)
  thinking = blocks.select { |block| THINKING_BLOCK_TYPES.include?(block['type']) }
  { 'anthropic' => thinking } unless thinking.empty?
end

.prepend_thinking_blocks(content_blocks, msg) ⇒ Object



397
398
399
400
401
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 397

def prepend_thinking_blocks(content_blocks, msg)
  content_blocks.unshift(*format_thinking_blocks(msg))

  content_blocks
end

.prompt_cache_control(caching = nil) ⇒ Object



446
447
448
449
450
451
452
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 446

def prompt_cache_control(caching = nil)
  options = prompt_cache_options(caching)

  { type: CACHE_CONTROL_TYPE }.tap do |control|
    control[:ttl] = options[:ttl] if options[:ttl]
  end
end

.prompt_cache_options(caching) ⇒ Object

Raises:

  • (ArgumentError)


454
455
456
457
458
459
460
461
462
463
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 454

def prompt_cache_options(caching)
  return {} unless caching

  options = caching.to_h.transform_keys(&:to_sym)
  unsupported = options.keys - PROMPT_CACHE_OPTIONS
  return options if unsupported.empty?

  raise ArgumentError,
        "Anthropic prompt caching accepts :ttl, got #{format_cache_option_keys(unsupported)}"
end

.provider_file_attachable?(attachment) ⇒ Boolean

Returns:

  • (Boolean)


203
204
205
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 203

def provider_file_attachable?(attachment)
  attachment.image? || attachment.pdf? || attachment.text?
end

.provider_file_source?(payload) ⇒ Boolean

Returns:

  • (Boolean)


179
180
181
182
183
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 179

def provider_file_source?(payload)
  blocks = Array(payload[:messages]).flat_map { |message| Array(message[:content]) }
  blocks.concat(Array(payload[:system]))
  blocks.any? { |block| block.is_a?(Hash) && block[:source].is_a?(Hash) && block[:source][:type] == 'file' }
end

.provider_file_upload_limitObject



199
200
201
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 199

def provider_file_upload_limit
  ANTHROPIC_FILE_UPLOAD_LIMIT
end

.render_count_tokens_payload(messages, tools:, model:, tool_prefs: nil, thinking: nil, schema: nil, citations: false, caching: nil) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 55

def render_count_tokens_payload(messages, tools:, model:, tool_prefs: nil, thinking: nil, schema: nil,
                                citations: false, caching: nil)
  render_payload(
    messages,
    tools: tools,
    tool_prefs: tool_prefs,
    temperature: nil,
    model: model,
    schema: schema,
    thinking: thinking,
    citations: citations,
    caching: caching
  ).slice(*COUNT_TOKENS_KEYS)
end

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



37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 37

def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil,
                   schema: nil, thinking: nil, citations: false, caching: nil, tool_prefs: nil)
  warn_unsupported_citations(model) if citations && !model.supports?(:citations)
  tool_prefs ||= {}
  system_messages, chat_messages = separate_messages(messages)
  system_content = build_system_content(system_messages, caching:)

  build_base_payload(chat_messages, model, stream, thinking, citations: citations, caching:,
                                                             max_output_tokens:).tap do |payload|
    add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:, schema:)
    payload[:cache_control] = prompt_cache_control(caching) if caching
  end
end

.resolve_effort(thinking) ⇒ Object



536
537
538
539
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 536

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

.separate_messages(messages) ⇒ Object



81
82
83
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 81

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

.server_tool_block?(block) ⇒ Boolean

Any block that is not text, thinking, or a function tool_use is a provider-executed tool step. Matching by shape rather than by an allowlist keeps tools Anthropic ships later flowing through.

Returns:

  • (Boolean)


231
232
233
234
235
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 231

def server_tool_block?(block)
  type = block['type'].to_s
  type == 'server_tool_use' || type == 'mcp_tool_use' || type == 'compaction' ||
    type.end_with?('_tool_result')
end

.supports_provider_file_references?Boolean

Returns:

  • (Boolean)


191
192
193
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 191

def supports_provider_file_references?
  true
end

.thinking_mode(thinking, model, effort, max_tokens) ⇒ Object

Effort alone never turns thinking on; Claude needs a thinking block. Generations that take a budget get one sized from the effort with the levels Bedrock publishes. Generations without a budget option think adaptively.



504
505
506
507
508
509
510
511
512
513
514
515
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 504

def thinking_mode(thinking, model, effort, max_tokens)
  return { type: 'adaptive' } if thinking.enabled == true

  budget = thinking.budget || effort_budget(effort, model, max_tokens)
  mode = if budget
           { type: 'enabled', budget_tokens: budget }
         elsif adaptive_thinking?(thinking, effort, model)
           { type: 'adaptive' }
         end
  mode[:display] = thinking.display.to_s if mode && thinking.display
  mode
end

.warn_unsupported_citations(model) ⇒ Object



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

def warn_unsupported_citations(model)
  RubyLLM.logger.warn(
    "#{model.id} does not support citations according to the model registry. " \
    'with_citations may have no effect.'
  )
end