Module: RubyLLM::Providers::Bedrock::Chat

Included in:
RubyLLM::Providers::Bedrock
Defined in:
lib/ruby_llm/providers/bedrock/chat.rb

Overview

Chat methods for Bedrock Converse API.

Class Method Summary collapse

Class Method Details

.budget_reasoning_config(thinking) ⇒ Object



318
319
320
321
322
323
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 318

def budget_reasoning_config(thinking)
  budget = thinking.respond_to?(:budget) ? thinking.budget : thinking
  return nil unless budget.is_a?(Integer)

  { reasoning_config: { type: 'enabled', budget_tokens: budget } }
end

.build_output_config(schema) ⇒ Object



277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 277

def build_output_config(schema)
  return nil unless schema

  cleaned = RubyLLM::Utils.deep_dup(schema[:schema])
  cleaned.delete(:strict)
  cleaned.delete('strict')

  {
    textFormat: {
      type: 'json_schema',
      structure: {
        jsonSchema: {
          schema: JSON.generate(cleaned),
          name: schema[:name]
        }
      }
    }
  }
end

.completion_urlObject



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

def completion_url
  "/model/#{@model.id}/converse"
end

.default_input_schemaObject



393
394
395
396
397
398
399
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 393

def default_input_schema
  {
    'type' => 'object',
    'properties' => {},
    'required' => []
  }
end

.effort_reasoning_config(thinking) ⇒ Object



306
307
308
309
310
311
312
313
314
315
316
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 306

def effort_reasoning_config(thinking)
  effort = thinking.respond_to?(:effort) ? thinking.effort : nil
  effort = effort.to_s if effort
  return nil if effort.nil? || effort.empty? || effort == 'none'

  if reasoning_embedded?(@model)
    { reasoning_config: { type: 'enabled', reasoning_effort: effort } }
  else
    { reasoning_effort: effort }
  end
end

.input_tokens(usage) ⇒ Object



69
70
71
72
73
74
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 69

def input_tokens(usage)
  input_tokens = usage['inputTokens']
  return unless input_tokens

  [input_tokens.to_i - usage['cacheReadInputTokens'].to_i - usage['cacheWriteInputTokens'].to_i, 0].max
end

.normalize_tool_result_block(block) ⇒ Object



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

def normalize_tool_result_block(block)
  return nil unless block.is_a?(Hash)
  return block if tool_result_content_block?(block)

  nil
end

.parse_completion_response(response) ⇒ Object

rubocop:enable Metrics/ParameterLists,Lint/UnusedMethodArgument



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 46

def parse_completion_response(response)
  data = response.body
  return if data.nil? || data.empty?

  content_blocks = data.dig('output', 'message', 'content') || []
  usage = data['usage'] || {}
  thinking_text, thinking_signature = parse_thinking(content_blocks)

  Message.new(
    role: :assistant,
    content: parse_text_content(content_blocks),
    thinking: Thinking.build(text: thinking_text, signature: thinking_signature),
    tool_calls: parse_tool_calls(content_blocks),
    input_tokens: input_tokens(usage),
    output_tokens: usage['outputTokens'],
    cached_tokens: usage['cacheReadInputTokens'],
    cache_creation_tokens: usage['cacheWriteInputTokens'],
    thinking_tokens: usage['reasoningTokens'],
    model_id: data['modelId'],
    raw: response
  )
end

.parse_reasoning_content_block(block) ⇒ Object



364
365
366
367
368
369
370
371
372
373
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 364

def parse_reasoning_content_block(block)
  reasoning_content = block['reasoningContent']
  return [nil, nil] unless reasoning_content.is_a?(Hash)

  reasoning_text = reasoning_content['reasoningText'] || {}
  text = reasoning_text['text'].is_a?(String) ? reasoning_text['text'] : nil
  signature = reasoning_text['signature'] if reasoning_text['signature'].is_a?(String)
  signature ||= reasoning_content['redactedContent'] if reasoning_content['redactedContent'].is_a?(String)
  [text, signature]
end

.parse_text_content(content_blocks) ⇒ Object



346
347
348
349
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 346

def parse_text_content(content_blocks)
  text = content_blocks.filter_map { |block| block['text'] if block['text'].is_a?(String) }.join
  text.empty? ? nil : text
end

.parse_thinking(content_blocks) ⇒ Object



351
352
353
354
355
356
357
358
359
360
361
362
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 351

def parse_thinking(content_blocks)
  text = +''
  signature = nil

  content_blocks.each do |block|
    chunk_text, chunk_signature = parse_reasoning_content_block(block)
    text << chunk_text if chunk_text
    signature ||= chunk_signature
  end

  [text.empty? ? nil : text, signature]
end

.parse_tool_calls(content_blocks) ⇒ Object



375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 375

def parse_tool_calls(content_blocks)
  tool_calls = {}

  content_blocks.each do |block|
    tool_use = block['toolUse']
    next unless tool_use

    tool_call_id = tool_use['toolUseId']
    tool_calls[tool_call_id] = ToolCall.new(
      id: tool_call_id,
      name: tool_use['name'],
      arguments: tool_use['input'] || {}
    )
  end

  tool_calls.empty? ? nil : tool_calls
end

.render_additional_model_request_fields(thinking) ⇒ Object



268
269
270
271
272
273
274
275
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 268

def render_additional_model_request_fields(thinking)
  fields = {}

  reasoning_fields = render_reasoning_fields(thinking)
  fields = RubyLLM::Utils.deep_merge(fields, reasoning_fields) if reasoning_fields

  fields.empty? ? nil : fields
end

.render_content_tool_result_content(content) ⇒ Object



170
171
172
173
174
175
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 170

def render_content_tool_result_content(content)
  blocks = []
  blocks << text_tool_result_block(content.text) unless content.text.to_s.empty?
  content.attachments.each { |attachment| blocks << text_tool_result_block(attachment.for_llm) }
  blocks.empty? ? [text_tool_result_block(nil)] : blocks
end

.render_inference_config(_model, temperature) ⇒ Object



217
218
219
220
221
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 217

def render_inference_config(_model, temperature)
  config = {}
  config[:temperature] = temperature unless temperature.nil?
  config
end

.render_message_content(msg) ⇒ Object



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

def render_message_content(msg)
  if msg.content.is_a?(RubyLLM::Content::Raw)
    return render_raw_content(msg.content) if msg.role == :assistant

    return sanitize_non_assistant_raw_blocks(render_raw_content(msg.content))
  end

  blocks = []

  thinking_block = render_thinking_block(msg.thinking)
  blocks << thinking_block if msg.role == :assistant && thinking_block

  text_and_media_blocks = Media.render_content(msg.content, used_document_names: @used_document_names)
  blocks.concat(text_and_media_blocks) if text_and_media_blocks

  if msg.tool_call?
    msg.tool_calls.each_value do |tool_call|
      blocks << {
        toolUse: {
          toolUseId: tool_call.id,
          name: tool_call.name,
          input: tool_call.arguments
        }
      }
    end
  end

  blocks
end

.render_messages(messages) ⇒ Object



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 76

def render_messages(messages)
  rendered = []
  tool_result_blocks = []

  messages.each do |msg|
    if msg.tool_result?
      tool_result_blocks << render_tool_result_block(msg)
      next
    end

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

    message = render_non_tool_message(msg)
    rendered << message if message
  end

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

.render_non_tool_message(msg) ⇒ Object



99
100
101
102
103
104
105
106
107
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 99

def render_non_tool_message(msg)
  content = render_message_content(msg)
  return nil if content.empty?

  {
    role: render_role(msg.role),
    content: content
  }
end

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

rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 15

def render_payload(messages, tools:, temperature:, model:, stream: false,
                   schema: nil, thinking: nil, tool_prefs: nil)
  tool_prefs ||= {}
  @model = model
  @used_document_names = {}
  system_messages, chat_messages = messages.partition { |msg| msg.role == :system }
  payload = {
    messages: render_messages(chat_messages)
  }

  system_blocks = render_system(system_messages)
  payload[:system] = system_blocks unless system_blocks.empty?

  payload[:inferenceConfig] = render_inference_config(model, temperature)

  tool_config = render_tool_config(tools, tool_prefs)
  if tool_config
    payload[:toolConfig] = tool_config
    payload[:tools] = tool_config[:tools] # Internal mirror for shared payload inspections in specs.
  end

  additional_fields = render_additional_model_request_fields(thinking)
  payload[:additionalModelRequestFields] = additional_fields if additional_fields

  output_config = build_output_config(schema)
  payload[:outputConfig] = output_config if output_config

  payload
end

.render_raw_content(content) ⇒ Object



139
140
141
142
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 139

def render_raw_content(content)
  value = content.value
  value.is_a?(Array) ? value : [value]
end

.render_raw_tool_result_content(raw_value) ⇒ Object



183
184
185
186
187
188
189
190
191
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 183

def render_raw_tool_result_content(raw_value)
  blocks = raw_value.is_a?(Array) ? raw_value : [raw_value]

  normalized = blocks.filter_map do |block|
    normalize_tool_result_block(block)
  end

  normalized.empty? ? [{ text: raw_value.to_s }] : normalized
end

.render_reasoning_fields(thinking) ⇒ Object



297
298
299
300
301
302
303
304
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 297

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

  effort_config = effort_reasoning_config(thinking)
  return effort_config if effort_config

  budget_reasoning_config(thinking)
end

.render_role(role) ⇒ Object



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

def render_role(role)
  case role
  when :assistant then 'assistant'
  else 'user'
  end
end

.render_system(messages) ⇒ Object



213
214
215
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 213

def render_system(messages)
  messages.flat_map { |msg| Media.render_content(msg.content, used_document_names: @used_document_names) }
end

.render_thinking_block(thinking) ⇒ Object



325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 325

def render_thinking_block(thinking)
  return nil unless thinking

  if thinking.text
    {
      reasoningContent: {
        reasoningText: {
          text: thinking.text,
          signature: thinking.signature
        }.compact
      }
    }
  elsif thinking.signature
    {
      reasoningContent: {
        redactedContent: thinking.signature
      }
    }
  end
end

.render_tool(tool) ⇒ Object



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

def render_tool(tool)
  input_schema = tool.params_schema || RubyLLM::Tool::SchemaDefinition.from_parameters(tool.parameters)&.json_schema

  tool_spec = {
    toolSpec: {
      name: tool.name,
      description: tool.description,
      inputSchema: {
        json: input_schema || default_input_schema
      }
    }
  }

  return tool_spec if tool.provider_params.empty?

  RubyLLM::Utils.deep_merge(tool_spec, tool.provider_params)
end

.render_tool_choice(choice) ⇒ Object



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

def render_tool_choice(choice)
  case choice
  when :auto
    { auto: {} }
  when :none
    nil
  when :required
    { any: {} }
  else
    { tool: { name: choice.to_s } }
  end
end

.render_tool_config(tools, tool_prefs) ⇒ Object



223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 223

def render_tool_config(tools, tool_prefs)
  return nil if tools.empty?

  config = {
    tools: tools.values.map { |tool| render_tool(tool) }
  }

  return config if tool_prefs.nil? || tool_prefs[:choice].nil?

  tool_choice = render_tool_choice(tool_prefs[:choice])
  config[:toolChoice] = tool_choice if tool_choice
  config
end

.render_tool_result_block(msg) ⇒ Object



153
154
155
156
157
158
159
160
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 153

def render_tool_result_block(msg)
  {
    toolResult: {
      toolUseId: msg.tool_call_id,
      content: render_tool_result_content(msg.content)
    }
  }
end

.render_tool_result_content(content) ⇒ Object



162
163
164
165
166
167
168
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 162

def render_tool_result_content(content)
  return render_raw_tool_result_content(content.value) if content.is_a?(RubyLLM::Content::Raw)
  return [{ json: content }] if content.is_a?(Hash) || content.is_a?(Array)
  return render_content_tool_result_content(content) if content.is_a?(RubyLLM::Content)

  [text_tool_result_block(content)]
end

.sanitize_non_assistant_raw_blocks(blocks) ⇒ Object



144
145
146
147
148
149
150
151
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 144

def sanitize_non_assistant_raw_blocks(blocks)
  blocks.filter_map do |block|
    next unless block.is_a?(Hash)
    next if block.key?(:reasoningContent) || block.key?('reasoningContent')

    block
  end
end

.text_tool_result_block(text) ⇒ Object



177
178
179
180
181
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 177

def text_tool_result_block(text)
  text = text.to_s
  text = '(no output)' if text.empty?
  { text: text }
end

.tool_result_content_block?(block) ⇒ Boolean

Returns:

  • (Boolean)


200
201
202
203
204
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 200

def tool_result_content_block?(block)
  %w[text json document image].any? do |key|
    block.key?(key) || block.key?(key.to_sym)
  end
end