ruby_llm 1.9.2 → 1.10.0

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -2
  3. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +3 -0
  4. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +1 -0
  5. data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +19 -0
  6. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +50 -0
  7. data/lib/ruby_llm/active_record/acts_as_legacy.rb +5 -1
  8. data/lib/ruby_llm/active_record/chat_methods.rb +12 -0
  9. data/lib/ruby_llm/active_record/message_methods.rb +41 -8
  10. data/lib/ruby_llm/aliases.json +0 -12
  11. data/lib/ruby_llm/chat.rb +10 -7
  12. data/lib/ruby_llm/configuration.rb +1 -1
  13. data/lib/ruby_llm/message.rb +37 -11
  14. data/lib/ruby_llm/models.json +1059 -857
  15. data/lib/ruby_llm/models.rb +134 -12
  16. data/lib/ruby_llm/provider.rb +4 -3
  17. data/lib/ruby_llm/providers/anthropic/chat.rb +128 -13
  18. data/lib/ruby_llm/providers/anthropic/streaming.rb +25 -1
  19. data/lib/ruby_llm/providers/bedrock/chat.rb +58 -15
  20. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +59 -2
  21. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +5 -0
  22. data/lib/ruby_llm/providers/gemini/chat.rb +69 -3
  23. data/lib/ruby_llm/providers/gemini/streaming.rb +32 -1
  24. data/lib/ruby_llm/providers/gemini/tools.rb +16 -3
  25. data/lib/ruby_llm/providers/gpustack/chat.rb +1 -1
  26. data/lib/ruby_llm/providers/mistral/chat.rb +58 -1
  27. data/lib/ruby_llm/providers/ollama/chat.rb +1 -1
  28. data/lib/ruby_llm/providers/openai/capabilities.rb +6 -2
  29. data/lib/ruby_llm/providers/openai/chat.rb +87 -3
  30. data/lib/ruby_llm/providers/openai/streaming.rb +11 -3
  31. data/lib/ruby_llm/providers/openai/temperature.rb +28 -0
  32. data/lib/ruby_llm/providers/openai.rb +1 -1
  33. data/lib/ruby_llm/providers/openrouter/chat.rb +154 -0
  34. data/lib/ruby_llm/providers/openrouter/streaming.rb +74 -0
  35. data/lib/ruby_llm/providers/openrouter.rb +2 -0
  36. data/lib/ruby_llm/providers/vertexai.rb +5 -1
  37. data/lib/ruby_llm/stream_accumulator.rb +111 -14
  38. data/lib/ruby_llm/streaming.rb +54 -51
  39. data/lib/ruby_llm/thinking.rb +49 -0
  40. data/lib/ruby_llm/tokens.rb +47 -0
  41. data/lib/ruby_llm/tool_call.rb +6 -3
  42. data/lib/ruby_llm/version.rb +1 -1
  43. data/lib/tasks/models.rake +19 -12
  44. metadata +12 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3767ec948dc2525f68824d04df633fb65bdb6dc5ea87464e059bee1559d6751e
4
- data.tar.gz: de82f54d7ebca42419671df2233c4a935eb17f17a8edd9095361f7a6107d8f71
3
+ metadata.gz: 5c88c057a541d5c27c5ab61a9bcf6db9e20c702fdb34b5b26d2d3154128f8dbd
4
+ data.tar.gz: d425e78899c69aa798838f0f3bf6e6f99f3e3ddc9c9057ded3bd7360b482acd1
5
5
  SHA512:
6
- metadata.gz: 5cce23683b7712a9d670f8ad7cf78c72a69663e788767ed2eddc36f4bde53c4d78a95801a59632dff702bd6b32e352d42e555564965709c6b19b4102160e3894
7
- data.tar.gz: 2456901875eda20c8593ce9bb6627bf4436b434c534c65e31b8241b99d2a253e651fedfd07df1eca1e38da32dffed8353d339b100047ecfac5118430e2e018f4
6
+ metadata.gz: 4545a9ef9254ec489924172c3f82fec9fd59986517e61b0e6ab24465dd5e4cd44f35296f14da3e3416c71bea05a1147ac80bf206fde68e071ae525c5e6effd27
7
+ data.tar.gz: 9be86b4db70aba3d4684dc81bac6949895c9232a0783f3b1f13758356565de682c4d8b9a1ff1aac70ceedcde9fd363b0a5b4e941516d26aafbc783d0e7692dd2
data/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <strong>One *beautiful* Ruby API for GPT, Claude, Gemini, and more.</strong>
9
9
 
10
- Battle tested at [<picture><source media="(prefers-color-scheme: dark)" srcset="https://chatwithwork.com/logotype-dark.svg"><img src="https://chatwithwork.com/logotype.svg" alt="Chat with Work" height="30" align="absmiddle"></picture>](https://chatwithwork.com) — *Claude Code for your documents*
10
+ Battle tested at [<picture><source media="(prefers-color-scheme: dark)" srcset="https://chatwithwork.com/logotype-dark.svg"><img src="https://chatwithwork.com/logotype.svg" alt="Chat with Work" height="30" align="absmiddle"></picture>](https://chatwithwork.com) — *Your AI coworker*
11
11
 
12
12
  [![Gem Version](https://badge.fury.io/rb/ruby_llm.svg?a=10)](https://badge.fury.io/rb/ruby_llm)
13
13
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop/rubocop)
@@ -122,7 +122,8 @@ response = chat.with_schema(ProductSchema).ask "Analyze this product", with: "pr
122
122
  * **Streaming:** Real-time responses with blocks
123
123
  * **Rails:** ActiveRecord integration with `acts_as_chat`
124
124
  * **Async:** Fiber-based concurrency
125
- * **Model registry:** 500+ models with capability detection and pricing
125
+ * **Model registry:** 800+ models with capability detection and pricing
126
+ * **Extended thinking:** Control, view, and persist model deliberation
126
127
  * **Providers:** OpenAI, Anthropic, Gemini, VertexAI, Bedrock, DeepSeek, Mistral, Ollama, OpenRouter, Perplexity, GPUStack, and any OpenAI-compatible API
127
128
 
128
129
  ## Installation
@@ -4,6 +4,9 @@ class Create<%= message_model_name.gsub('::', '').pluralize %> < ActiveRecord::M
4
4
  t.string :role, null: false
5
5
  t.text :content
6
6
  t.json :content_raw
7
+ t.text :thinking_text
8
+ t.text :thinking_signature
9
+ t.integer :thinking_tokens
7
10
  t.integer :input_tokens
8
11
  t.integer :output_tokens
9
12
  t.integer :cached_tokens
@@ -4,6 +4,7 @@ class Create<%= tool_call_model_name.gsub('::', '').pluralize %> < ActiveRecord:
4
4
  create_table :<%= tool_call_table_name %> do |t|
5
5
  t.string :tool_call_id, null: false
6
6
  t.string :name, null: false
7
+ t.string :thought_signature
7
8
  <% if postgresql? %>
8
9
  t.jsonb :arguments, default: {}
9
10
  <% elsif mysql? %>
@@ -0,0 +1,19 @@
1
+ class AddRubyLlmV110Columns < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ unless column_exists?(:<%= message_table_name %>, :thinking_text)
4
+ add_column :<%= message_table_name %>, :thinking_text, :text
5
+ end
6
+
7
+ unless column_exists?(:<%= message_table_name %>, :thinking_signature)
8
+ add_column :<%= message_table_name %>, :thinking_signature, :text
9
+ end
10
+
11
+ unless column_exists?(:<%= message_table_name %>, :thinking_tokens)
12
+ add_column :<%= message_table_name %>, :thinking_tokens, :integer
13
+ end
14
+
15
+ unless column_exists?(:<%= tool_call_table_name %>, :thought_signature)
16
+ add_column :<%= tool_call_table_name %>, :thought_signature, :string
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+ require_relative '../generator_helpers'
6
+
7
+ module RubyLLM
8
+ module Generators
9
+ # Generator to add v1.10 columns (thinking output + thinking tokens) to existing apps.
10
+ class UpgradeToV110Generator < Rails::Generators::Base
11
+ include Rails::Generators::Migration
12
+ include RubyLLM::Generators::GeneratorHelpers
13
+
14
+ namespace 'ruby_llm:upgrade_to_v1_10'
15
+ source_root File.expand_path('templates', __dir__)
16
+
17
+ argument :model_mappings, type: :array, default: [], banner: 'message:MessageName'
18
+
19
+ desc 'Adds thinking output columns and thinking token tracking introduced in v1.10.0'
20
+
21
+ def self.next_migration_number(dirname)
22
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
23
+ end
24
+
25
+ def create_migration_file
26
+ parse_model_mappings
27
+
28
+ migration_template 'add_v1_10_message_columns.rb.tt',
29
+ 'db/migrate/add_ruby_llm_v1_10_columns.rb',
30
+ migration_version: migration_version,
31
+ message_table_name: message_table_name,
32
+ tool_call_table_name: tool_call_table_name
33
+ end
34
+
35
+ def show_next_steps
36
+ say_status :success, 'Upgrade prepared!', :green
37
+ say <<~INSTRUCTIONS
38
+
39
+ Next steps:
40
+ 1. Review the generated migration
41
+ 2. Run: rails db:migrate
42
+ 3. Restart your application server
43
+
44
+ 📚 See the v1.10.0 release notes for details on extended thinking support.
45
+
46
+ INSTRUCTIONS
47
+ end
48
+ end
49
+ end
50
+ end
@@ -279,8 +279,11 @@ module RubyLLM
279
279
  end
280
280
 
281
281
  def persist_tool_calls(tool_calls)
282
+ supports_thought_signature = tool_calls.klass.column_names.include?('thought_signature')
283
+
282
284
  tool_calls.each_value do |tool_call|
283
285
  attributes = tool_call.to_h
286
+ attributes.delete(:thought_signature) unless supports_thought_signature
284
287
  attributes[:tool_call_id] = attributes.delete(:id)
285
288
  @message.tool_calls.create!(**attributes)
286
289
  end
@@ -357,7 +360,8 @@ module RubyLLM
357
360
  RubyLLM::ToolCall.new(
358
361
  id: tool_call.tool_call_id,
359
362
  name: tool_call.name,
360
- arguments: tool_call.arguments
363
+ arguments: tool_call.arguments,
364
+ thought_signature: tool_call.try(:thought_signature)
361
365
  )
362
366
  ]
363
367
  end
@@ -124,6 +124,11 @@ module RubyLLM
124
124
  self
125
125
  end
126
126
 
127
+ def with_thinking(...)
128
+ to_llm.with_thinking(...)
129
+ self
130
+ end
131
+
127
132
  def with_params(...)
128
133
  to_llm.with_params(...)
129
134
  self
@@ -262,6 +267,9 @@ module RubyLLM
262
267
  if @message.has_attribute?(:cache_creation_tokens)
263
268
  attrs[:cache_creation_tokens] = message.cache_creation_tokens
264
269
  end
270
+ attrs[:thinking_text] = message.thinking&.text if @message.has_attribute?(:thinking_text)
271
+ attrs[:thinking_signature] = message.thinking&.signature if @message.has_attribute?(:thinking_signature)
272
+ attrs[:thinking_tokens] = message.thinking_tokens if @message.has_attribute?(:thinking_tokens)
265
273
 
266
274
  # Add model association dynamically
267
275
  attrs[self.class.model_association_name] = model_association
@@ -282,8 +290,12 @@ module RubyLLM
282
290
  # rubocop:enable Metrics/PerceivedComplexity
283
291
 
284
292
  def persist_tool_calls(tool_calls)
293
+ tool_call_klass = @message.tool_calls_association.klass
294
+ supports_thought_signature = tool_call_klass.column_names.include?('thought_signature')
295
+
285
296
  tool_calls.each_value do |tool_call|
286
297
  attributes = tool_call.to_h
298
+ attributes.delete(:thought_signature) unless supports_thought_signature
287
299
  attributes[:tool_call_id] = attributes.delete(:id)
288
300
  @message.tool_calls_association.create!(**attributes)
289
301
  end
@@ -11,24 +11,56 @@ module RubyLLM
11
11
  end
12
12
 
13
13
  def to_llm
14
- cached = has_attribute?(:cached_tokens) ? self[:cached_tokens] : nil
15
- cache_creation = has_attribute?(:cache_creation_tokens) ? self[:cache_creation_tokens] : nil
16
-
17
14
  RubyLLM::Message.new(
18
15
  role: role.to_sym,
19
16
  content: extract_content,
17
+ thinking: thinking,
18
+ tokens: tokens,
20
19
  tool_calls: extract_tool_calls,
21
20
  tool_call_id: extract_tool_call_id,
22
- input_tokens: input_tokens,
23
- output_tokens: output_tokens,
24
- cached_tokens: cached,
25
- cache_creation_tokens: cache_creation,
26
21
  model_id: model_association&.model_id
27
22
  )
28
23
  end
29
24
 
25
+ def thinking
26
+ RubyLLM::Thinking.build(
27
+ text: thinking_text_value,
28
+ signature: thinking_signature_value
29
+ )
30
+ end
31
+
32
+ def tokens
33
+ RubyLLM::Tokens.build(
34
+ input: input_tokens,
35
+ output: output_tokens,
36
+ cached: cached_value,
37
+ cache_creation: cache_creation_value,
38
+ thinking: thinking_tokens_value
39
+ )
40
+ end
41
+
30
42
  private
31
43
 
44
+ def thinking_text_value
45
+ has_attribute?(:thinking_text) ? self[:thinking_text] : nil
46
+ end
47
+
48
+ def thinking_signature_value
49
+ has_attribute?(:thinking_signature) ? self[:thinking_signature] : nil
50
+ end
51
+
52
+ def cached_value
53
+ has_attribute?(:cached_tokens) ? self[:cached_tokens] : nil
54
+ end
55
+
56
+ def cache_creation_value
57
+ has_attribute?(:cache_creation_tokens) ? self[:cache_creation_tokens] : nil
58
+ end
59
+
60
+ def thinking_tokens_value
61
+ has_attribute?(:thinking_tokens) ? self[:thinking_tokens] : nil
62
+ end
63
+
32
64
  def extract_tool_calls
33
65
  tool_calls_association.to_h do |tool_call|
34
66
  [
@@ -36,7 +68,8 @@ module RubyLLM
36
68
  RubyLLM::ToolCall.new(
37
69
  id: tool_call.tool_call_id,
38
70
  name: tool_call.name,
39
- arguments: tool_call.arguments
71
+ arguments: tool_call.arguments,
72
+ thought_signature: tool_call.try(:thought_signature)
40
73
  )
41
74
  ]
42
75
  end
@@ -336,18 +336,6 @@
336
336
  "openai": "gpt-5.2-pro",
337
337
  "openrouter": "openai/gpt-5.2-pro"
338
338
  },
339
- "imagen-4.0-fast-generate-001": {
340
- "gemini": "imagen-4.0-fast-generate-001",
341
- "vertexai": "imagen-4.0-fast-generate-001"
342
- },
343
- "imagen-4.0-generate-001": {
344
- "gemini": "imagen-4.0-generate-001",
345
- "vertexai": "imagen-4.0-generate-001"
346
- },
347
- "imagen-4.0-ultra-generate-001": {
348
- "gemini": "imagen-4.0-ultra-generate-001",
349
- "vertexai": "imagen-4.0-ultra-generate-001"
350
- },
351
339
  "o1": {
352
340
  "openai": "o1",
353
341
  "openrouter": "openai/o1"
data/lib/ruby_llm/chat.rb CHANGED
@@ -22,6 +22,7 @@ module RubyLLM
22
22
  @params = {}
23
23
  @headers = {}
24
24
  @schema = nil
25
+ @thinking = nil
25
26
  @on = {
26
27
  new_message: nil,
27
28
  end_message: nil,
@@ -67,6 +68,13 @@ module RubyLLM
67
68
  self
68
69
  end
69
70
 
71
+ def with_thinking(effort: nil, budget: nil)
72
+ raise ArgumentError, 'with_thinking requires :effort or :budget' if effort.nil? && budget.nil?
73
+
74
+ @thinking = Thinking::Config.new(effort: effort, budget: budget)
75
+ self
76
+ end
77
+
70
78
  def with_context(context)
71
79
  @context = context
72
80
  @config = context.config
@@ -130,6 +138,7 @@ module RubyLLM
130
138
  params: @params,
131
139
  headers: @headers,
132
140
  schema: @schema,
141
+ thinking: @thinking,
133
142
  &wrap_streaming_block(&)
134
143
  )
135
144
 
@@ -172,15 +181,9 @@ module RubyLLM
172
181
  def wrap_streaming_block(&block)
173
182
  return nil unless block_given?
174
183
 
175
- first_chunk_received = false
184
+ @on[:new_message]&.call
176
185
 
177
186
  proc do |chunk|
178
- # Create message on first content chunk
179
- unless first_chunk_received
180
- first_chunk_received = true
181
- @on[:new_message]&.call
182
- end
183
-
184
187
  block.call chunk
185
188
  end
186
189
  end
@@ -56,7 +56,7 @@ module RubyLLM
56
56
  @retry_interval_randomness = 0.5
57
57
  @http_proxy = nil
58
58
 
59
- @default_model = 'gpt-4.1-nano'
59
+ @default_model = 'gpt-5-nano'
60
60
  @default_embedding_model = 'text-embedding-3-small'
61
61
  @default_moderation_model = 'omni-moderation-latest'
62
62
  @default_image_model = 'gpt-image-1'
@@ -5,8 +5,7 @@ module RubyLLM
5
5
  class Message
6
6
  ROLES = %i[system user assistant tool].freeze
7
7
 
8
- attr_reader :role, :model_id, :tool_calls, :tool_call_id, :input_tokens, :output_tokens,
9
- :cached_tokens, :cache_creation_tokens, :raw
8
+ attr_reader :role, :model_id, :tool_calls, :tool_call_id, :raw, :thinking, :tokens
10
9
  attr_writer :content
11
10
 
12
11
  def initialize(options = {})
@@ -15,11 +14,16 @@ module RubyLLM
15
14
  @model_id = options[:model_id]
16
15
  @tool_calls = options[:tool_calls]
17
16
  @tool_call_id = options[:tool_call_id]
18
- @input_tokens = options[:input_tokens]
19
- @output_tokens = options[:output_tokens]
20
- @cached_tokens = options[:cached_tokens]
21
- @cache_creation_tokens = options[:cache_creation_tokens]
17
+ @tokens = options[:tokens] || Tokens.build(
18
+ input: options[:input_tokens],
19
+ output: options[:output_tokens],
20
+ cached: options[:cached_tokens],
21
+ cache_creation: options[:cache_creation_tokens],
22
+ thinking: options[:thinking_tokens],
23
+ reasoning: options[:reasoning_tokens]
24
+ )
22
25
  @raw = options[:raw]
26
+ @thinking = options[:thinking]
23
27
 
24
28
  ensure_valid_role
25
29
  end
@@ -44,6 +48,30 @@ module RubyLLM
44
48
  content if tool_result?
45
49
  end
46
50
 
51
+ def input_tokens
52
+ tokens&.input
53
+ end
54
+
55
+ def output_tokens
56
+ tokens&.output
57
+ end
58
+
59
+ def cached_tokens
60
+ tokens&.cached
61
+ end
62
+
63
+ def cache_creation_tokens
64
+ tokens&.cache_creation
65
+ end
66
+
67
+ def thinking_tokens
68
+ tokens&.thinking
69
+ end
70
+
71
+ def reasoning_tokens
72
+ tokens&.thinking
73
+ end
74
+
47
75
  def to_h
48
76
  {
49
77
  role: role,
@@ -51,11 +79,9 @@ module RubyLLM
51
79
  model_id: model_id,
52
80
  tool_calls: tool_calls,
53
81
  tool_call_id: tool_call_id,
54
- input_tokens: input_tokens,
55
- output_tokens: output_tokens,
56
- cached_tokens: cached_tokens,
57
- cache_creation_tokens: cache_creation_tokens
58
- }.compact
82
+ thinking: thinking&.text,
83
+ thinking_signature: thinking&.signature
84
+ }.merge(tokens ? tokens.to_h : {}).compact
59
85
  end
60
86
 
61
87
  def instance_variables