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

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



62
63
64
65
66
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 62

def add_optional_fields(payload, system_content:, tools:, temperature:)
  payload[:tools] = tools.values.map { |t| Tools.function_for(t) } if tools.any?
  payload[:system] = system_content unless system_content.empty?
  payload[:temperature] = temperature unless temperature.nil?
end

.append_formatted_content(content_blocks, content) ⇒ Object



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

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
60
# 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
  }

  thinking_payload = build_thinking_payload(thinking)
  payload[:thinking] = thinking_payload if thinking_payload

  payload
end

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

rubocop:disable Metrics/ParameterLists



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 97

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_system_content(system_messages) ⇒ Object



27
28
29
30
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 27

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

  if system_messages.length > 1
    RubyLLM.logger.warn(
      "Anthropic's Claude implementation only supports a single system message. " \
      'Multiple system messages will be combined into one.'
    )
  end

  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



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 188

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

Raises:

  • (ArgumentError)


221
222
223
224
225
226
227
228
229
230
231
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 221

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

  budget = resolve_budget(thinking)
  raise ArgumentError, 'Anthropic thinking requires a budget' if budget.nil?

  {
    type: 'enabled',
    budget_tokens: budget
  }
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



214
215
216
217
218
219
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 214

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

.extract_text_content(blocks) ⇒ Object



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

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



85
86
87
88
89
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 85

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



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

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



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

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



124
125
126
127
128
129
130
131
132
133
134
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 124

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



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

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)
  content_blocks << Media.format_text(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



68
69
70
71
72
73
74
75
76
77
78
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 68

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



179
180
181
182
183
184
185
186
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 179

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

rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument



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

def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil) # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument
  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:, temperature:)
  end
end

.resolve_budget(thinking) ⇒ Object



233
234
235
236
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 233

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

.separate_messages(messages) ⇒ Object



23
24
25
# File 'lib/ruby_llm/providers/anthropic/chat.rb', line 23

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