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
- .adaptive_thinking?(thinking, effort, model) ⇒ Boolean
- .add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:, schema: nil) ⇒ Object
- .add_thinking_fields(payload, thinking, model) ⇒ Object
- .append_formatted_content(content_blocks, msg, citations: false) ⇒ Object
-
.apply_compaction(payload, compaction) ⇒ Object
Anthropic compacts through a context_management edit.
- .apply_compaction_headers(headers, _compaction) ⇒ Object
- .apply_end_user(payload, identifier) ⇒ Object
-
.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.
- .build_base_payload(chat_messages, model, stream, thinking, citations: false, caching: nil, max_output_tokens: nil) ⇒ Object
- .build_message(data, content:, citations:, thinking:, thinking_signature:, tool_use_blocks:, raw:, server_tool_calls: [], raw_content: nil) ⇒ Object
- .build_output_config(schema) ⇒ Object
- .build_system_content(system_messages, caching: nil) ⇒ Object
- .build_thinking_block(thinking) ⇒ Object
- .build_thinking_payload(thinking, model, max_tokens) ⇒ Object
- .cache_boundary?(message, caching: nil) ⇒ Boolean
-
.citation_url(data) ⇒ Object
Search result citations carry the developer-provided source string.
- .compaction_edit(compaction) ⇒ Object
- .completion_url ⇒ Object
- .convert_role(role) ⇒ Object
- .count_tokens_url ⇒ Object
- .default_large_file_upload_threshold ⇒ Object
- .effort_budget(effort, model, max_tokens) ⇒ Object
- .extract_server_tool_calls(blocks) ⇒ Object
- .extract_text_and_citations(blocks) ⇒ Object
-
.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.
- .extract_thinking_signature(blocks) ⇒ Object
- .finish_reasons ⇒ Object
- .format_basic_message_with_thinking(msg, citations: false, caching: nil) ⇒ Object
- .format_cache_option_keys(keys) ⇒ Object
-
.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.
- .format_messages(messages, thinking: nil, citations: false, caching: nil) ⇒ Object
-
.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.
- .format_thinking_blocks(msg) ⇒ Object
- .format_tool_call_with_thinking(msg, caching: nil) ⇒ Object
- .inject_cache_control(blocks, caching: nil) ⇒ Object
-
.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.
- .normalize_finish_reason(reason) ⇒ Object
- .parse_citation(data, text: nil, start_index: nil, end_index: nil) ⇒ Object
- .parse_completion_body(data, raw:) ⇒ Object
- .parse_count_tokens_response(response) ⇒ Object
- .parse_thinking_blocks(blocks) ⇒ Object
- .prepend_thinking_blocks(content_blocks, msg) ⇒ Object
- .prompt_cache_control(caching = nil) ⇒ Object
- .prompt_cache_options(caching) ⇒ Object
- .provider_file_attachable?(attachment) ⇒ Boolean
- .provider_file_source?(payload) ⇒ Boolean
- .provider_file_upload_limit ⇒ Object
- .render_count_tokens_payload(messages, tools:, model:, tool_prefs: nil, thinking: nil, schema: nil, citations: false, caching: nil) ⇒ Object
- .render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil, schema: nil, thinking: nil, citations: false, caching: nil, tool_prefs: nil) ⇒ Object
- .resolve_effort(thinking) ⇒ Object
- .separate_messages(messages) ⇒ Object
-
.server_tool_block?(block) ⇒ Boolean
Any block that is not text, thinking, or a function tool_use is a provider-executed tool step.
- .supports_provider_file_references? ⇒ Boolean
-
.thinking_mode(thinking, model, effort, max_tokens) ⇒ Object
Effort alone never turns thinking on; Claude needs a thinking block.
- .warn_unsupported_citations(model) ⇒ Object
Class Method Details
.adaptive_thinking?(thinking, effort, model) ⇒ 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., 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(, model, stream, thinking, citations: false, caching: nil, max_output_tokens: nil) payload = { model: model.id, 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 (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(, caching: nil) return [] if .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. .flat_map do |msg| blocks = Media.format_content(msg.content, msg.).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
431 432 433 |
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 431 def cache_boundary?(, caching: nil) caching != false && .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_url ⇒ Object
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_url ⇒ Object
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_threshold ⇒ Object
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_reasons ⇒ Object
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 (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 (msg, thinking: nil, citations: false, caching: nil) # rubocop:disable Lint/UnusedMethodArgument if msg.role == :assistant && msg.raw_content (msg, caching:) elsif msg.tool_call? format_tool_call_with_thinking(msg, caching:) elsif msg.tool_result? Tools.format_tool_result(msg) else (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 (, thinking: nil, citations: false, caching: nil) rendered = [] tool_result_blocks = [] .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 = (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 (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) (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) = (caching) { type: CACHE_CONTROL_TYPE }.tap do |control| control[:ttl] = [:ttl] if [:ttl] end end |
.prompt_cache_options(caching) ⇒ Object
454 455 456 457 458 459 460 461 462 463 |
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 454 def (caching) return {} unless caching = caching.to_h.transform_keys(&:to_sym) unsupported = .keys - PROMPT_CACHE_OPTIONS return if unsupported.empty? raise ArgumentError, "Anthropic prompt caching accepts :ttl, got #{format_cache_option_keys(unsupported)}" end |
.provider_file_attachable?(attachment) ⇒ Boolean
203 204 205 |
# File 'lib/ruby_llm/protocols/anthropic/chat.rb', line 203 def provider_file_attachable?() .image? || .pdf? || .text? end |
.provider_file_source?(payload) ⇒ 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 { || Array([: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_limit ⇒ Object
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(, tools:, model:, tool_prefs: nil, thinking: nil, schema: nil, citations: false, caching: nil) render_payload( , 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(, 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_content = build_system_content(, caching:) build_base_payload(, 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 () .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.
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
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 |