ruby_llm 2.0.0.rc1 → 2.0.0.rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,6 +18,7 @@ module RubyLLM
18
18
  COMPACTION_BETA = 'compact-2026-01-12'
19
19
  COMPACTION_EDIT_TYPE = 'compact_20260112'
20
20
  COUNT_TOKENS_KEYS = %i[model messages system tools tool_choice thinking].freeze
21
+ THINKING_BLOCK_TYPES = %w[thinking redacted_thinking].freeze
21
22
 
22
23
  module_function
23
24
 
@@ -303,6 +304,11 @@ module RubyLLM
303
304
  thinking_block&.dig('signature') || thinking_block&.dig('data')
304
305
  end
305
306
 
307
+ def parse_thinking_blocks(blocks)
308
+ thinking = blocks.select { |block| THINKING_BLOCK_TYPES.include?(block['type']) }
309
+ { 'anthropic' => thinking } unless thinking.empty?
310
+ end
311
+
306
312
  def build_message(data, content:, citations:, thinking:, thinking_signature:, tool_use_blocks:, raw:,
307
313
  server_tool_calls: [], raw_content: nil)
308
314
  usage = aggregate_usage(data['usage'])
@@ -316,6 +322,7 @@ module RubyLLM
316
322
  content: content,
317
323
  citations: citations,
318
324
  thinking: Thinking.build(text: thinking, signature: thinking_signature),
325
+ raw_reasoning: parse_thinking_blocks(data['content'] || []),
319
326
  tool_calls: Tools.parse_tool_calls(tool_use_blocks),
320
327
  server_tool_calls: server_tool_calls,
321
328
  raw_content: raw_content,
@@ -357,12 +364,7 @@ module RubyLLM
357
364
  end
358
365
 
359
366
  def format_basic_message_with_thinking(msg, citations: false, caching: nil)
360
- content_blocks = []
361
-
362
- if msg.role == :assistant
363
- thinking_block = build_thinking_block(msg.thinking)
364
- content_blocks << thinking_block if thinking_block
365
- end
367
+ content_blocks = msg.role == :assistant ? format_thinking_blocks(msg) : []
366
368
 
367
369
  append_formatted_content(content_blocks, msg, citations: citations)
368
370
  inject_cache_control(content_blocks, caching:) if cache_boundary?(msg, caching:)
@@ -374,7 +376,7 @@ module RubyLLM
374
376
  end
375
377
 
376
378
  def format_tool_call_with_thinking(msg, caching: nil)
377
- content_blocks = prepend_thinking_block([], msg)
379
+ content_blocks = prepend_thinking_blocks([], msg)
378
380
  append_formatted_content(content_blocks, msg) unless msg.content.nil? || msg.content.empty?
379
381
 
380
382
  msg.tool_calls.each_value do |tool_call|
@@ -393,13 +395,19 @@ module RubyLLM
393
395
  }
394
396
  end
395
397
 
396
- def prepend_thinking_block(content_blocks, msg)
397
- thinking_block = build_thinking_block(msg.thinking)
398
- content_blocks.unshift(thinking_block) if thinking_block
398
+ def prepend_thinking_blocks(content_blocks, msg)
399
+ content_blocks.unshift(*format_thinking_blocks(msg))
399
400
 
400
401
  content_blocks
401
402
  end
402
403
 
404
+ def format_thinking_blocks(msg)
405
+ blocks = msg.raw_reasoning['anthropic'] if msg.raw_reasoning.is_a?(Hash)
406
+ return Support::Utils.deep_dup(blocks) if blocks
407
+
408
+ [build_thinking_block(msg.thinking)].compact
409
+ end
410
+
403
411
  def build_thinking_block(thinking)
404
412
  return nil unless thinking
405
413
 
@@ -108,12 +108,13 @@ module RubyLLM
108
108
  end
109
109
 
110
110
  def stream_end_fields(data)
111
- return {} unless data['type'] == 'message_stop' && @saw_server_block
111
+ return {} unless data['type'] == 'message_stop'
112
112
 
113
- blocks = @stream_blocks.sort.map { |_index, block| block }
113
+ blocks = (@stream_blocks || {}).sort.map { |_index, block| block }
114
114
  {
115
115
  server_tool_calls: extract_server_tool_calls(blocks),
116
- raw_content: blocks
116
+ raw_content: @saw_server_block ? blocks : nil,
117
+ raw_reasoning: parse_thinking_blocks(blocks)
117
118
  }
118
119
  end
119
120
 
@@ -87,8 +87,9 @@ module RubyLLM
87
87
  end
88
88
 
89
89
  def merge_raw_content(segments)
90
- merged = segments.flat_map { |segment| segment.raw_content || [] }
91
- merged.empty? ? nil : merged
90
+ return unless segments.any?(&:raw_content)
91
+
92
+ segments.flat_map { |segment| segment.raw_content || format_message(segment)[:content] }
92
93
  end
93
94
 
94
95
  def merge_tool_calls(segments)
@@ -117,6 +117,7 @@ module RubyLLM
117
117
  content: text_content,
118
118
  citations: citations,
119
119
  thinking: Thinking.build(text: thinking_text, signature: thinking_signature),
120
+ raw_reasoning: parse_thinking_blocks(content_blocks),
120
121
  tool_calls: parse_tool_calls(content_blocks),
121
122
  server_tool_calls: extract_server_tool_calls(content_blocks),
122
123
  input_tokens: input_tokens(usage),
@@ -195,10 +196,7 @@ module RubyLLM
195
196
  end
196
197
 
197
198
  def format_structured_message_content(msg, citations: false)
198
- blocks = []
199
-
200
- thinking_block = format_thinking_block(msg.thinking)
201
- blocks << thinking_block if msg.role == :assistant && thinking_block
199
+ blocks = msg.role == :assistant ? format_thinking_blocks(msg) : []
202
200
 
203
201
  blocks.concat(
204
202
  Media.format_content(msg.content, msg.attachments, used_document_names: @used_document_names, citations:)
@@ -207,6 +205,13 @@ module RubyLLM
207
205
  blocks
208
206
  end
209
207
 
208
+ def format_thinking_blocks(msg)
209
+ blocks = msg.raw_reasoning['converse'] if msg.raw_reasoning.is_a?(Hash)
210
+ return Support::Utils.deep_dup(blocks) if blocks
211
+
212
+ [format_thinking_block(msg.thinking)].compact
213
+ end
214
+
210
215
  def format_tool_result_block(msg)
211
216
  {
212
217
  toolResult: {
@@ -614,6 +619,11 @@ module RubyLLM
614
619
  [text, signature]
615
620
  end
616
621
 
622
+ def parse_thinking_blocks(content_blocks)
623
+ blocks = content_blocks.select { |block| block['reasoningContent'].is_a?(Hash) }
624
+ { 'converse' => blocks } unless blocks.empty?
625
+ end
626
+
617
627
  def parse_reasoning_content_block(block)
618
628
  reasoning_content = block['reasoningContent']
619
629
  return [nil, nil] unless reasoning_content.is_a?(Hash)
@@ -18,6 +18,7 @@ module RubyLLM
18
18
  end
19
19
 
20
20
  def stream_response(payload, additional_headers = {}, &block)
21
+ @thinking_stream = ThinkingStream.new
21
22
  accumulator = RubyLLM::Protocol::StreamAccumulator.new
22
23
  decoder = event_stream_decoder
23
24
  body = JSON.generate(payload)
@@ -195,6 +196,7 @@ module RubyLLM
195
196
  text: extract_thinking_delta(event),
196
197
  signature: extract_thinking_signature(event)
197
198
  ),
199
+ raw_reasoning: streamed_thinking_blocks(event),
198
200
  tool_calls: extract_tool_calls(event),
199
201
  server_tool_calls: extract_server_tool_call_events(event),
200
202
  input_tokens: extract_input_tokens(metadata_usage, usage),
@@ -206,6 +208,12 @@ module RubyLLM
206
208
  )
207
209
  end
208
210
 
211
+ def streamed_thinking_blocks(event)
212
+ @thinking_stream ||= ThinkingStream.new
213
+ @thinking_stream.add(event)
214
+ @thinking_stream.raw_reasoning if event.key?('messageStop') || event.key?('stopReason')
215
+ end
216
+
209
217
  def extract_finish_reason(event)
210
218
  normalize_finish_reason(event.dig('messageStop', 'stopReason') || event['stopReason'])
211
219
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module RubyLLM
6
+ module Protocols
7
+ class Converse
8
+ class ThinkingStream # :nodoc: all
9
+ def initialize
10
+ @blocks = {}
11
+ end
12
+
13
+ def add(event)
14
+ @blocks.clear if event.key?('messageStart')
15
+ part = event['contentBlockStart'] || event['contentBlockDelta'] || event
16
+ index = part['contentBlockIndex']
17
+ return if index.nil?
18
+
19
+ start = part.dig('start', 'reasoningContent')
20
+ @blocks[index] = Support::Utils.deep_dup(start) if start
21
+ delta = part.dig('delta', 'reasoningContent')
22
+ append_delta(index, delta) if delta
23
+ end
24
+
25
+ def raw_reasoning
26
+ return if @blocks.empty?
27
+
28
+ { 'converse' => @blocks.sort.map { |_index, content| { 'reasoningContent' => content } } }
29
+ end
30
+
31
+ private
32
+
33
+ def append_delta(index, delta)
34
+ block = (@blocks[index] ||= {})
35
+ if delta.key?('redactedContent')
36
+ append_redacted(block, delta['redactedContent'])
37
+ else
38
+ text = (block['reasoningText'] ||= { 'text' => '' })
39
+ (delta['reasoningText'] || delta).slice('text', 'signature').each do |key, value|
40
+ text[key] = text[key].to_s + value.to_s
41
+ end
42
+ end
43
+ end
44
+
45
+ def append_redacted(block, data)
46
+ block['redactedContent'] = if block['redactedContent']
47
+ Base64.strict_encode64(Base64.decode64(block['redactedContent']) +
48
+ Base64.decode64(data))
49
+ else
50
+ data
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -80,7 +80,7 @@ module RubyLLM
80
80
 
81
81
  def format_thinking(msg)
82
82
  return {} unless msg.role == :assistant
83
- return { reasoning_details: msg.raw_reasoning } if msg.raw_reasoning
83
+ return { reasoning_details: msg.raw_reasoning } if msg.raw_reasoning.is_a?(Array)
84
84
 
85
85
  thinking = msg.thinking
86
86
  return {} unless thinking
@@ -2,5 +2,5 @@
2
2
 
3
3
  module RubyLLM
4
4
  # The version of the ruby_llm gem, as a string.
5
- VERSION = '2.0.0.rc1'
5
+ VERSION = '2.0.0.rc2'
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0.rc1
4
+ version: 2.0.0.rc2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino
@@ -379,6 +379,7 @@ files:
379
379
  - lib/ruby_llm/protocols/converse/chat.rb
380
380
  - lib/ruby_llm/protocols/converse/media.rb
381
381
  - lib/ruby_llm/protocols/converse/streaming.rb
382
+ - lib/ruby_llm/protocols/converse/thinking_stream.rb
382
383
  - lib/ruby_llm/protocols/deepgram.rb
383
384
  - lib/ruby_llm/protocols/deepgram/models.rb
384
385
  - lib/ruby_llm/protocols/deepgram/speech.rb
@@ -611,7 +612,7 @@ metadata:
611
612
  funding_uri: https://github.com/sponsors/crmne
612
613
  rubygems_mfa_required: 'true'
613
614
  post_install_message: |
614
- RubyLLM 2.0 release candidate 1
615
+ RubyLLM 2.0.0.rc2
615
616
 
616
617
  2.0 renames several APIs and changes what message content returns. Coming
617
618
  from 1.x? Read the upgrade guide before you boot: