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



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

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

.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



337
338
339
340
341
342
343
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 337

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

.effort_reasoning_config(thinking) ⇒ Object



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

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

.normalize_tool_result_block(block) ⇒ Object



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

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



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 40

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: usage['inputTokens'],
    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



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

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



290
291
292
293
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 290

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



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

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



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 319

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



232
233
234
235
236
237
238
239
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 232

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_inference_config(_model, temperature) ⇒ Object



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

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

.render_message_content(msg) ⇒ Object



96
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
123
124
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 96

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



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 63

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



86
87
88
89
90
91
92
93
94
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 86

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

rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument



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

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

  payload
end

.render_raw_content(content) ⇒ Object



126
127
128
129
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 126

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

.render_raw_tool_result_content(raw_value) ⇒ Object



166
167
168
169
170
171
172
173
174
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 166

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



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

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



189
190
191
192
193
194
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 189

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

.render_system(messages) ⇒ Object



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

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



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 269

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



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 214

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_config(tools) ⇒ Object



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

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

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

.render_tool_result_block(msg) ⇒ Object



140
141
142
143
144
145
146
147
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 140

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



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

def render_tool_result_content(content)
  return render_raw_tool_result_content(content.value) if content.is_a?(RubyLLM::Content::Raw)

  if content.is_a?(Hash) || content.is_a?(Array)
    [{ json: content }]
  elsif content.is_a?(RubyLLM::Content)
    blocks = []
    blocks << { text: content.text } if content.text
    content.attachments.each do |attachment|
      blocks << { text: attachment.for_llm }
    end
    blocks
  else
    [{ text: content.to_s }]
  end
end

.sanitize_non_assistant_raw_blocks(blocks) ⇒ Object



131
132
133
134
135
136
137
138
# File 'lib/ruby_llm/providers/bedrock/chat.rb', line 131

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

.tool_result_content_block?(block) ⇒ Boolean

Returns:

  • (Boolean)


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

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