ruby_llm 2.0.0.rc4 → 2.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c047801a2b7a5a642296f0ae84e81ab29b641353ffe8618b3ece44db0b0a3849
4
- data.tar.gz: 05e4624f14a8e2045408dd21007758612f0644522bae4e250902fd60d66955e1
3
+ metadata.gz: 42dd9dfdb910f8e5df4eb6114ed59586a13e4a51797173d3f3bdb13b6d59c28b
4
+ data.tar.gz: fa4e0aa374c915ab5e97146bcacb438fc2c3fca0c029ed51c1aea6c934769da8
5
5
  SHA512:
6
- metadata.gz: 6e73e5f6818e70c607c3cabfc6e0dfb19f9271491375b71279e0a205f2ecff8fe0051c691c51230644d32f7ad8f377b8f8bddfcae919e33ff64015ccd9a942e7
7
- data.tar.gz: fd4285734764621dc2cab77579b8c713aaff907930c1b6849472c470839b2a75e6975832baf4e43adeedd1d3326b92af816af4a63242bdfa2585fab2b72fdc58
6
+ metadata.gz: 3e5489b6fdd071de5c4281c9cc8eff9d345dd10876cb6773144873d2e58c3ec400109d208706d4c04c12d93328f5864f839907521fd751accc29aa9a5a753602
7
+ data.tar.gz: 19c33af936f0626262ad3e469c955b98aee8599b15fe088ba745e31584f91ebbf166cabc981bef4bef526f6e6c96800aebe07dc133ca732313ca58598aa6273f
data/README.md CHANGED
@@ -34,7 +34,7 @@ Build with the models you want. Move between hosted and local providers without
34
34
 
35
35
  ## Start with one line. Add files, tools, and agents
36
36
 
37
- These examples use **2.0.0.rc4 (prerelease)**. Follow [Getting Started](https://rubyllm.com/getting-started/) to install it and configure the providers you want to try. For 1.x, use the [stable-version docs](https://rubyllm.com/v1/).
37
+ These examples use **2.0.0**. Follow [Getting Started](https://rubyllm.com/getting-started/) to install it and configure the providers you want to try. For 1.x, use the [1.x docs](https://rubyllm.com/v1/).
38
38
 
39
39
  ```ruby
40
40
  # Just ask
@@ -167,7 +167,7 @@ Agents, workflows, RAG, images, audio, and video. Built in, with usage tracking
167
167
  * **Tools:** Let AI call your Ruby methods
168
168
  * **Tool approval:** Park a run until a human approves with `requires_approval`
169
169
  * **The agentic loop:** Drive it yourself with `ask_later`, `step`, and `complete?`
170
- * **Server tools:** Web search, code execution, and MCP connectors with `with_server_tools`
170
+ * **Provider tools:** Web search, code execution, and MCP connectors with `with_provider_tools`
171
171
  * **Agents:** Reusable assistants with `RubyLLM::Agent`
172
172
  * **Prompt templates:** ERB prompts in `app/prompts`, rendered with `RubyLLM.render_prompt`
173
173
  * **Workflows:** Correlate multi-agent runs in your telemetry with `RubyLLM.workflow`
@@ -189,10 +189,10 @@ Agents, workflows, RAG, images, audio, and video. Built in, with usage tracking
189
189
 
190
190
  ## Installation
191
191
 
192
- Install the 2.0 release candidate:
192
+ Install RubyLLM 2.0:
193
193
 
194
194
  ```bash
195
- bundle add ruby_llm --version 2.0.0.rc4
195
+ bundle add ruby_llm --version 2.0.0
196
196
  ```
197
197
 
198
198
  Configure a provider in your script, or in `config/initializers/ruby_llm.rb` in Rails:
@@ -138,7 +138,10 @@ module RubyLLM
138
138
  end
139
139
 
140
140
  def reference_type
141
- Rails.application.config.generators.options.dig(:active_record, :primary_key_type) || :bigint
141
+ application = Rails.application if Rails.respond_to?(:application)
142
+ return :bigint unless application
143
+
144
+ application.config.generators.options.dig(:active_record, :primary_key_type) || :bigint
142
145
  end
143
146
 
144
147
  def create_migration_class_name(table_name)
@@ -1,6 +1,6 @@
1
1
  class <%= create_migration_class_name(chat_table_name) %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
- create_table :<%= chat_table_name %> do |t|
3
+ create_table :<%= chat_table_name %>, id: :<%= reference_type %> do |t|
4
4
  t.references :ruby_llm_model, null: false, foreign_key: { to_table: :ruby_llm_models }, type: :<%= reference_type %>
5
5
  t.boolean :cancelled, null: false, default: false
6
6
  t.timestamps
@@ -1,6 +1,6 @@
1
1
  class <%= create_migration_class_name(message_table_name) %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
- create_table :<%= message_table_name %> do |t|
3
+ create_table :<%= message_table_name %>, id: :<%= reference_type %> do |t|
4
4
  t.references :<%= chat_table_name.singularize %>, null: false, foreign_key: { to_table: :<%= chat_table_name %> }, type: :<%= reference_type %>
5
5
  t.string :role, null: false
6
6
  t.text :content
@@ -1,6 +1,6 @@
1
- class CreateRubyLlmRecords < ActiveRecord::Migration<%= migration_version %>
1
+ class <%= create_migration_class_name('ruby_llm_records') %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
- create_table :ruby_llm_models do |t|
3
+ create_table :ruby_llm_models, id: :<%= reference_type %> do |t|
4
4
  t.string :model_id, null: false
5
5
  t.string :name, null: false
6
6
  t.string :provider, null: false
@@ -37,7 +37,7 @@ class CreateRubyLlmRecords < ActiveRecord::Migration<%= migration_version %>
37
37
  <% end -%>
38
38
  end
39
39
 
40
- create_table :ruby_llm_tool_calls do |t|
40
+ create_table :ruby_llm_tool_calls, id: :<%= reference_type %> do |t|
41
41
  t.references :message, polymorphic: true, null: false, type: :<%= reference_type %>, index: false
42
42
  t.references :result, polymorphic: true, type: :<%= reference_type %>, index: false
43
43
  t.string :tool_call_id, null: false
@@ -60,7 +60,7 @@ class CreateRubyLlmRecords < ActiveRecord::Migration<%= migration_version %>
60
60
  t.index :name
61
61
  end
62
62
 
63
- create_table :ruby_llm_usages do |t|
63
+ create_table :ruby_llm_usages, id: :<%= reference_type %> do |t|
64
64
  t.references :chat, polymorphic: true, null: false, type: :<%= reference_type %>, index: false
65
65
  t.references :message, polymorphic: true, type: :<%= reference_type %>, index: false
66
66
  t.string :operation, null: false
@@ -87,7 +87,7 @@ class CreateRubyLlmRecords < ActiveRecord::Migration<%= migration_version %>
87
87
  t.check_constraint "status IN (<%= usage_statuses_sql %>)"
88
88
  end
89
89
 
90
- create_table :ruby_llm_batches do |t|
90
+ create_table :ruby_llm_batches, id: :<%= reference_type %> do |t|
91
91
  t.string :provider_batch_id, null: false
92
92
  t.string :provider, null: false
93
93
  t.string :status, null: false
@@ -382,7 +382,7 @@ class <%= prepare_migration_class_name %> < ActiveRecord::Migration<%= migration
382
382
 
383
383
  def create_usages
384
384
  unless table_exists?(:ruby_llm_usages)
385
- create_table :ruby_llm_usages do |table|
385
+ create_table :ruby_llm_usages, id: :<%= reference_type %> do |table|
386
386
  table.references :chat, polymorphic: true, null: false,
387
387
  type: reference_type_for(:<%= chat_table_name %>), index: false
388
388
  table.references :message, polymorphic: true,
@@ -417,7 +417,7 @@ class <%= prepare_migration_class_name %> < ActiveRecord::Migration<%= migration
417
417
 
418
418
  def create_batches
419
419
  unless table_exists?(:ruby_llm_batches)
420
- create_table :ruby_llm_batches do |table|
420
+ create_table :ruby_llm_batches, id: :<%= reference_type %> do |table|
421
421
  table.string :provider_batch_id, null: false
422
422
  table.string :provider, null: false
423
423
  table.string :status, null: false
@@ -58,18 +58,96 @@ module RubyLLM
58
58
  end
59
59
 
60
60
  def plain_text_content(content_value)
61
+ return action_text_plain_text(content_value) if action_text_content?(content_value)
61
62
  return content_value.to_plain_text if content_value.respond_to?(:to_plain_text)
62
63
 
63
64
  content_value
64
65
  end
65
66
 
66
67
  def action_text_attachment_sources(content_value)
67
- return [] unless content_value.respond_to?(:body)
68
+ return [] unless action_text_content?(content_value)
68
69
 
70
+ action_text_attachables(content_value).flat_map do |attachable|
71
+ action_text_attachable_sources(attachable)
72
+ end.compact
73
+ end
74
+
75
+ def action_text_content?(content_value)
76
+ content_value.respond_to?(:body) && action_text_body?(content_value.body)
77
+ end
78
+
79
+ def preload_action_text_embeds(messages)
80
+ return unless defined?(ActionText::Attachment)
81
+
82
+ rich_texts = messages.filter_map do |message|
83
+ rich_text = message.rich_text_content
84
+ body = rich_text&.body
85
+ rich_text if action_text_body?(body) && action_text_embeds?(body)
86
+ end
87
+ return if rich_texts.empty?
88
+
89
+ ::ActiveRecord::Associations::Preloader.new(
90
+ records: rich_texts,
91
+ associations: { embeds_attachments: :blob }
92
+ ).call
93
+ end
94
+
95
+ def action_text_embeds?(body)
96
+ body.fragment.find_all(ActionText::Attachment.tag_name).any? { |node| action_text_blob_id(node) }
97
+ end
98
+
99
+ def action_text_body?(body)
100
+ defined?(ActionText::Attachment) && body.respond_to?(:fragment) && body.respond_to?(:attachables) &&
101
+ body.respond_to?(:sanitize_content_attachment)
102
+ end
103
+
104
+ def action_text_plain_text(content_value)
105
+ body = content_value.body
106
+ preloaded_blobs = action_text_preloaded_blobs(content_value).index_by { |blob| blob.id.to_s }
107
+ rendered = body.fragment.replace(ActionText::Attachment.tag_name) do |node|
108
+ if node.key?('content')
109
+ sanitized_content = body.sanitize_content_attachment(node.remove_attribute('content').to_s)
110
+ node['content'] = sanitized_content if sanitized_content.present?
111
+ end
112
+
113
+ attachable = action_text_attachable_for_node(node, preloaded_blobs)
114
+ ActionText::Attachment.from_node(node, attachable).to_plain_text
115
+ end
116
+
117
+ ActionText::Content.new(rendered, canonicalize: false).fragment.to_plain_text
118
+ end
119
+
120
+ def action_text_attachables(content_value)
69
121
  body = content_value.body
70
- return [] unless body.respond_to?(:attachables)
122
+ preloaded_blobs = action_text_preloaded_blobs(content_value).index_by { |blob| blob.id.to_s }
123
+ body.fragment.find_all(ActionText::Attachment.tag_name).map do |node|
124
+ action_text_attachable_for_node(node, preloaded_blobs)
125
+ end
126
+ end
71
127
 
72
- body.attachables.flat_map { |attachable| action_text_attachable_sources(attachable) }.compact
128
+ def action_text_attachable_for_node(node, preloaded_blobs)
129
+ preloaded_blobs[action_text_blob_id(node)] || ActionText::Attachable.from_node(node)
130
+ end
131
+
132
+ def action_text_blob_id(node)
133
+ sgid = node['sgid']
134
+ gid = SignedGlobalID.parse(sgid, for: ActionText::Attachable::LOCATOR_NAME) if sgid
135
+ return unless gid && gid.app == GlobalID.app && gid.model_name == ActiveStorage::Blob.name
136
+
137
+ gid.model_id.to_s
138
+ end
139
+
140
+ def action_text_preloaded_blobs(content_value)
141
+ return [] unless content_value.class.respond_to?(:reflect_on_association) &&
142
+ content_value.class.reflect_on_association(:embeds_attachments)
143
+
144
+ association = content_value.association(:embeds_attachments)
145
+ return [] unless association.loaded?
146
+
147
+ content_value.embeds_attachments.filter_map do |attachment|
148
+ blob_association = attachment.association(:blob)
149
+ blob_association.target if blob_association.loaded?
150
+ end
73
151
  end
74
152
 
75
153
  def action_text_attachable_sources(attachable)
@@ -212,7 +212,7 @@ module RubyLLM
212
212
  # RubyLLM::Chat. Each behaves exactly as documented on RubyLLM::Chat,
213
213
  # then returns the record so calls chain.
214
214
  CHAINABLE_CHAT_DELEGATES = %i[
215
- with_tools with_tool_options with_server_tools with_fallbacks with_temperature
215
+ with_tools with_tool_options with_provider_tools with_fallbacks with_temperature
216
216
  with_max_output_tokens with_thinking with_citations with_caching
217
217
  with_end_user with_compaction
218
218
  with_provider_options with_headers with_schema
@@ -233,10 +233,10 @@ module RubyLLM
233
233
  # Applies Chat#with_tool_options and returns this record.
234
234
 
235
235
  ##
236
- # :method: with_server_tools
237
- # :call-seq: with_server_tools(*tools, **tools_with_options)
236
+ # :method: with_provider_tools
237
+ # :call-seq: with_provider_tools(*tools, **tools_with_options)
238
238
  #
239
- # Applies Chat#with_server_tools and returns this record.
239
+ # Applies Chat#with_provider_tools and returns this record.
240
240
 
241
241
  ##
242
242
  # :method: with_fallbacks
@@ -407,10 +407,10 @@ module RubyLLM
407
407
  # Delegates to Chat#schema. See that method for arguments and return values.
408
408
 
409
409
  ##
410
- # :method: server_tools
411
- # :call-seq: server_tools
410
+ # :method: provider_tools
411
+ # :call-seq: provider_tools
412
412
  #
413
- # Delegates to Chat#server_tools. See that method for arguments and return values.
413
+ # Delegates to Chat#provider_tools. See that method for arguments and return values.
414
414
 
415
415
  ##
416
416
  # :method: temperature
@@ -449,7 +449,7 @@ module RubyLLM
449
449
 
450
450
  PASSTHROUGH_CHAT_DELEGATES = %i[
451
451
  caching citations compaction concurrency end_user fallbacks headers max_output_tokens provider_options
452
- schema server_tools temperature thinking tool_options tools
452
+ schema provider_tools temperature thinking tool_options tools
453
453
  add_completion count_tokens each render
454
454
  ].freeze
455
455
 
@@ -778,8 +778,11 @@ module RubyLLM
778
778
 
779
779
  associations = %i[ruby_llm_tool_calls ruby_llm_parent_tool_call ruby_llm_usages]
780
780
  associations << { attachments_attachments: :blob } if attachment_association?(assoc.klass)
781
+ rich_text_reflection = assoc.klass.reflect_on_association(:rich_text_content)
782
+ associations << :rich_text_content if rich_text_reflection
781
783
 
782
784
  ::ActiveRecord::Associations::Preloader.new(records: messages, associations: associations).call
785
+ preload_action_text_embeds(messages) if rich_text_reflection
783
786
  messages
784
787
  end
785
788
 
@@ -44,7 +44,7 @@ module RubyLLM
44
44
  DUPED_INHERITED_CONFIG = {
45
45
  :@chat_kwargs => {},
46
46
  :@tools => [],
47
- :@server_tools => [],
47
+ :@provider_tools => [],
48
48
  :@tool_options => {},
49
49
  :@caching => nil,
50
50
  :@compaction => nil,
@@ -66,7 +66,7 @@ module RubyLLM
66
66
 
67
67
  # Chat methods that return the wrapped chat so calls can be chained there.
68
68
  CHAINABLE_CHAT_DELEGATES = %i[
69
- with_instructions with_tools with_server_tools with_tool_options with_model
69
+ with_instructions with_tools with_provider_tools with_tool_options with_model
70
70
  with_temperature with_max_output_tokens with_thinking with_citations
71
71
  with_end_user with_compaction with_caching with_context with_provider_options
72
72
  with_headers with_schema with_fallbacks
@@ -77,7 +77,7 @@ module RubyLLM
77
77
 
78
78
  # Chat values and operations whose return values pass through unchanged.
79
79
  PASSTHROUGH_CHAT_DELEGATES = %i[
80
- model provider messages tools server_tools tool_options provider_options headers schema concurrency
80
+ model provider messages tools provider_tools tool_options provider_options headers schema concurrency
81
81
  caching citations compaction context end_user fallbacks thinking temperature max_output_tokens
82
82
  each complete? cancelled? awaiting_approval? pending_approvals
83
83
  add_message add_completion tokens cost render
@@ -150,17 +150,17 @@ module RubyLLM
150
150
  end
151
151
 
152
152
  # Enables provider-executed tools for chats this agent builds, applied
153
- # via Chat#with_server_tools. Accepts the same aliases, options, and
153
+ # via Chat#with_provider_tools. Accepts the same aliases, options, and
154
154
  # raw Hashes; a block defers evaluation until the chat is built.
155
155
  # Called with no arguments, returns the declared entries.
156
156
  #
157
- # server_tools :web_search
158
- # server_tools web_search: { allowed_domains: ["ruby-lang.org"] }
157
+ # provider_tools :web_search
158
+ # provider_tools web_search: { allowed_domains: ["ruby-lang.org"] }
159
159
  #
160
- def server_tools(*tools, **tools_with_options, &block)
161
- return @server_tools || [] if tools.empty? && tools_with_options.empty? && !block_given?
160
+ def provider_tools(*tools, **tools_with_options, &block)
161
+ return @provider_tools || [] if tools.empty? && tools_with_options.empty? && !block_given?
162
162
 
163
- @server_tools = block_given? ? block : RubyLLM::Tools::ServerTools.normalize(tools, tools_with_options)
163
+ @provider_tools = block_given? ? block : RubyLLM::Tools::ProviderTools.normalize(tools, tools_with_options)
164
164
  end
165
165
 
166
166
  # Adds system instructions for chats this agent builds. Accepts a string,
@@ -667,8 +667,8 @@ module RubyLLM
667
667
  options = evaluate(tool_options, runtime)
668
668
  chat.with_tool_options(**options) if options && !options.empty?
669
669
 
670
- server_tools_to_apply = Array(evaluate(server_tools, runtime)).compact
671
- chat.with_server_tools(*server_tools_to_apply) if server_tools_to_apply.any?
670
+ provider_tools_to_apply = Array(evaluate(provider_tools, runtime)).compact
671
+ chat.with_provider_tools(*provider_tools_to_apply) if provider_tools_to_apply.any?
672
672
  end
673
673
 
674
674
  def apply_passthrough_options(chat)
@@ -873,10 +873,10 @@ module RubyLLM
873
873
  # Delegates to Chat#with_tools. See that method for arguments and return values.
874
874
 
875
875
  ##
876
- # :method: with_server_tools
877
- # :call-seq: with_server_tools(*tools, **tools_with_options)
876
+ # :method: with_provider_tools
877
+ # :call-seq: with_provider_tools(*tools, **tools_with_options)
878
878
  #
879
- # Delegates to Chat#with_server_tools. See that method for arguments and return values.
879
+ # Delegates to Chat#with_provider_tools. See that method for arguments and return values.
880
880
 
881
881
  ##
882
882
  # :method: with_tool_options
@@ -1053,10 +1053,10 @@ module RubyLLM
1053
1053
  # Delegates to Chat#tools. See that method for arguments and return values.
1054
1054
 
1055
1055
  ##
1056
- # :method: server_tools
1057
- # :call-seq: server_tools
1056
+ # :method: provider_tools
1057
+ # :call-seq: provider_tools
1058
1058
  #
1059
- # Delegates to Chat#server_tools. See that method for arguments and return values.
1059
+ # Delegates to Chat#provider_tools. See that method for arguments and return values.
1060
1060
 
1061
1061
  ##
1062
1062
  # :method: tool_options
data/lib/ruby_llm/chat.rb CHANGED
@@ -42,9 +42,9 @@ module RubyLLM
42
42
  # The registered tools, as a Hash of tool name Symbols to Tool instances.
43
43
  attr_reader :tools
44
44
 
45
- # The server tools enabled with #with_server_tools, as an array of
45
+ # The provider tools enabled with #with_provider_tools, as an array of
46
46
  # normalized entry Hashes.
47
- attr_reader :server_tools
47
+ attr_reader :provider_tools
48
48
 
49
49
  # Extra request options set with #with_provider_options, expressed in
50
50
  # the provider's request vocabulary.
@@ -119,7 +119,7 @@ module RubyLLM
119
119
  @messages = []
120
120
  @usage_entries = []
121
121
  @tools = {}
122
- @server_tools = []
122
+ @provider_tools = []
123
123
  @tool_prefs = { choice: nil, calls: nil }
124
124
  @concurrency = normalize_tool_concurrency(@config.tool_concurrency)
125
125
  @provider_options = {}
@@ -354,10 +354,10 @@ module RubyLLM
354
354
  # no alias for yet work without a gem update. Entries add to any tools
355
355
  # enabled earlier; pass +nil+ to clear them all. Returns +self+.
356
356
  #
357
- # chat.with_server_tools(:web_search)
358
- # chat.with_server_tools(:web_search, :code_execution)
359
- # chat.with_server_tools(web_search: { allowed_domains: ["ruby-lang.org"] })
360
- # chat.with_server_tools({ type: "web_search_20260318", name: "web_search" })
357
+ # chat.with_provider_tools(:web_search)
358
+ # chat.with_provider_tools(:web_search, :code_execution)
359
+ # chat.with_provider_tools(web_search: { allowed_domains: ["ruby-lang.org"] })
360
+ # chat.with_provider_tools({ type: "web_search_20260318", name: "web_search" })
361
361
  #
362
362
  # The tool steps the model ran come back on
363
363
  # Message#server_tool_calls, citations from search tools on
@@ -365,14 +365,14 @@ module RubyLLM
365
365
  # <tt>message.tokens.server_tool_use</tt>.
366
366
  #
367
367
  # Raises UnsupportedServerToolError at request time when the provider
368
- # has no server-tool support or does not define a requested alias.
369
- def with_server_tools(*tools, **tools_with_options)
368
+ # has no provider-tool support or does not define a requested alias.
369
+ def with_provider_tools(*tools, **tools_with_options)
370
370
  if tools == [nil] && tools_with_options.empty?
371
- @server_tools = []
371
+ @provider_tools = []
372
372
  return self
373
373
  end
374
374
 
375
- @server_tools += RubyLLM::Tools::ServerTools.normalize(tools, tools_with_options)
375
+ @provider_tools += RubyLLM::Tools::ProviderTools.normalize(tools, tools_with_options)
376
376
  self
377
377
  end
378
378
 
@@ -742,7 +742,7 @@ module RubyLLM
742
742
  # chat.with_instructions("Be terse.").with_tools(Weather)
743
743
  # chat.count_tokens("What's the weather in Berlin?")
744
744
  #
745
- # Server tools, provider_options, compaction, and before_request hooks
745
+ # Provider tools, provider_options, compaction, and before_request hooks
746
746
  # are not included. Raises Error when the provider has no token counting
747
747
  # endpoint.
748
748
  def count_tokens(message = nil)
@@ -849,7 +849,7 @@ module RubyLLM
849
849
  @provider.render(
850
850
  preprocessed_messages,
851
851
  tools: @tools,
852
- server_tools: @server_tools,
852
+ provider_tools: @provider_tools,
853
853
  tool_prefs: @tool_prefs,
854
854
  temperature: @temperature,
855
855
  max_output_tokens: @max_output_tokens,
@@ -1009,7 +1009,7 @@ module RubyLLM
1009
1009
  input_messages: messages.dup,
1010
1010
  message_count: messages.size,
1011
1011
  tools: tools.keys,
1012
- server_tools: server_tools,
1012
+ provider_tools: provider_tools,
1013
1013
  tool_choice: tool_prefs[:choice],
1014
1014
  tool_call_limit: tool_prefs[:calls],
1015
1015
  temperature: @temperature,
@@ -1147,7 +1147,7 @@ module RubyLLM
1147
1147
  @provider.complete(
1148
1148
  preprocessed_messages,
1149
1149
  tools: @tools,
1150
- server_tools: @server_tools,
1150
+ provider_tools: @provider_tools,
1151
1151
  tool_prefs: @tool_prefs,
1152
1152
  temperature: @temperature,
1153
1153
  max_output_tokens: @max_output_tokens,
@@ -83,7 +83,7 @@ module RubyLLM
83
83
  end
84
84
  end
85
85
 
86
- # Raised when Chat#with_server_tools is used with a provider RubyLLM has
86
+ # Raised when Chat#with_provider_tools is used with a provider RubyLLM has
87
87
  # no server-tool support for, or with an alias the provider's protocol
88
88
  # does not define.
89
89
  class UnsupportedServerToolError < Error; end
@@ -64,7 +64,7 @@ module RubyLLM
64
64
 
65
65
  # The provider-executed tool steps in this response, as an array of
66
66
  # ServerToolCall objects. Empty unless the chat enabled tools with
67
- # Chat#with_server_tools and the model used one.
67
+ # Chat#with_provider_tools and the model used one.
68
68
  attr_reader :server_tool_calls
69
69
 
70
70
  # The provider-shaped content blocks of this assistant message, kept
@@ -129,6 +129,14 @@ module RubyLLM
129
129
  dup.tap { |message| message.instance_variable_set(:@attachments, wrapped) }
130
130
  end
131
131
 
132
+ def without_thinking # :nodoc:
133
+ dup.tap do |message|
134
+ message.instance_variable_set(:@thinking, nil)
135
+ message.instance_variable_set(:@raw_reasoning, nil)
136
+ message.instance_variable_set(:@tool_calls, tool_calls_without_thought_signatures)
137
+ end
138
+ end
139
+
132
140
  # Returns +true+ if the assistant requested one or more tool calls,
133
141
  # +false+ otherwise.
134
142
  def tool_call?
@@ -276,6 +284,12 @@ module RubyLLM
276
284
  end
277
285
  end
278
286
 
287
+ def tool_calls_without_thought_signatures
288
+ tool_calls&.transform_values do |call|
289
+ call.dup.tap { |copy| copy.thought_signature = nil }
290
+ end
291
+ end
292
+
279
293
  def coerce_thinking(thinking, signature)
280
294
  case thinking
281
295
  when nil, Thinking then thinking
@@ -96,13 +96,13 @@ module RubyLLM
96
96
 
97
97
  def complete(messages, tools:, temperature:, provider_options: {}, headers: {}, schema: nil, thinking: nil,
98
98
  max_output_tokens: nil, citations: false, caching: nil, tool_prefs: nil, before_request: [],
99
- usage_recorder: nil, server_tools: [], compaction: nil, end_user: nil, &)
100
- resolution = resolve_server_tools_for_request(server_tools)
99
+ usage_recorder: nil, provider_tools: [], compaction: nil, end_user: nil, &)
100
+ resolution = resolve_provider_tools_for_request(provider_tools)
101
101
  headers = resolution.headers.merge(headers) if resolution
102
102
  headers = apply_compaction_headers(headers, compaction) if compaction
103
103
  payload = render(
104
104
  messages, tools:, tool_prefs:, temperature:, max_output_tokens:, provider_options:, schema:, thinking:,
105
- citations:, caching:, compaction:, end_user:, before_request:, server_tools:,
105
+ citations:, caching:, compaction:, end_user:, before_request:, provider_tools:,
106
106
  stream: block_given?
107
107
  )
108
108
 
@@ -122,7 +122,7 @@ module RubyLLM
122
122
 
123
123
  def render(messages, tools:, temperature:, provider_options: {}, schema: nil, thinking: nil,
124
124
  max_output_tokens: nil, citations: false, caching: nil, tool_prefs: nil, before_request: [],
125
- stream: false, server_tools: [], compaction: nil, end_user: nil)
125
+ stream: false, provider_tools: [], compaction: nil, end_user: nil)
126
126
  payload = render_payload(
127
127
  messages,
128
128
  tools: tools,
@@ -139,7 +139,7 @@ module RubyLLM
139
139
  payload = apply_end_user(payload, end_user) if end_user
140
140
  payload = apply_compaction(payload, compaction) if compaction
141
141
  payload = Support::Utils.deep_merge(payload, provider_options)
142
- payload = apply_server_tools(payload, server_tools)
142
+ payload = apply_provider_tools(payload, provider_tools)
143
143
  apply_before_request_hooks(payload, before_request)
144
144
  rescue NotImplementedError
145
145
  raise Error, "#{@provider.name} doesn't support chat"
@@ -188,8 +188,8 @@ module RubyLLM
188
188
  end
189
189
 
190
190
  # The alias table mapping portable server tool names to this protocol's
191
- # wire format. Protocols with server-tool support override this;
192
- # +nil+ means the protocol has no server-tool support at all.
191
+ # wire format. Protocols with provider-tool support override this;
192
+ # +nil+ means the protocol has no provider-tool support at all.
193
193
  def server_tool_aliases
194
194
  nil
195
195
  end
@@ -471,6 +471,7 @@ module RubyLLM
471
471
  end
472
472
 
473
473
  def preprocess_message(message)
474
+ return message.without_thinking if foreign_thinking?(message)
474
475
  return message unless auto_upload_large_files?
475
476
  return message unless message.role == :user
476
477
  return message if message.attachments.empty?
@@ -483,21 +484,43 @@ module RubyLLM
483
484
 
484
485
  private
485
486
 
486
- def resolve_server_tools_for_request(entries)
487
+ # A thinking signature is opaque to every provider but the one that
488
+ # issued it, so a message another provider produced replays without
489
+ # its thinking. A message with no known producer replays as it is.
490
+ def foreign_thinking?(message)
491
+ return false unless message.role == :assistant && carries_thinking?(message)
492
+
493
+ producer = producer_slug(message)
494
+ !producer.nil? && producer != @provider.slug
495
+ end
496
+
497
+ def carries_thinking?(message)
498
+ return true if message.thinking || message.raw_reasoning
499
+
500
+ message.tool_call? && message.tool_calls.each_value.any?(&:thought_signature)
501
+ end
502
+
503
+ # Only a usage entry names the producer: a model id alone can belong
504
+ # to several providers.
505
+ def producer_slug(message)
506
+ message.ruby_llm_usage_entries.reverse.find(&:succeeded?)&.provider
507
+ end
508
+
509
+ def resolve_provider_tools_for_request(entries)
487
510
  return nil if entries.nil? || entries.empty?
488
511
 
489
512
  aliases = server_tool_aliases
490
513
  unless aliases
491
514
  raise UnsupportedServerToolError,
492
- "#{@provider.name} has no server-tool support through RubyLLM yet. " \
515
+ "#{@provider.name} has no provider-tool support through RubyLLM yet. " \
493
516
  'Request options in the provider vocabulary can be set with with_provider_options.'
494
517
  end
495
518
 
496
- RubyLLM::Tools::ServerTools.resolve(entries, aliases: aliases, owner: @provider.name)
519
+ RubyLLM::Tools::ProviderTools.resolve(entries, aliases: aliases, owner: @provider.name)
497
520
  end
498
521
 
499
- def apply_server_tools(payload, entries)
500
- resolution = resolve_server_tools_for_request(entries)
522
+ def apply_provider_tools(payload, entries)
523
+ resolution = resolve_provider_tools_for_request(entries)
501
524
  return payload unless resolution
502
525
 
503
526
  payload = Support::Utils.deep_merge(payload, resolution.payload) unless resolution.payload.empty?
@@ -505,7 +528,7 @@ module RubyLLM
505
528
  payload
506
529
  end
507
530
 
508
- # Server tools join function tools in the payload's tools array. The
531
+ # Provider tools join function tools in the payload's tools array. The
509
532
  # entry shape comes from the alias table or the caller's raw Hash.
510
533
  def merge_server_tool_entries(payload, entries)
511
534
  payload[:tools] = Array(payload[:tools]) + entries
@@ -21,9 +21,9 @@ module RubyLLM
21
21
 
22
22
  def server_tool_aliases = SERVER_TOOL_ALIASES
23
23
 
24
- def create_research_job(prompt, agent:, with: nil, server_tools: nil, provider_options: {})
24
+ def create_research_job(prompt, agent:, with: nil, provider_tools: nil, provider_options: {})
25
25
  validate_research_agent(agent)
26
- payload = render_research_payload(prompt, agent:, with:, server_tools:, provider_options:)
26
+ payload = render_research_payload(prompt, agent:, with:, provider_tools:, provider_options:)
27
27
  response = @connection.post(research_url, payload, idempotent: false)
28
28
  parse_research_job(response, agent:)
29
29
  end
@@ -71,20 +71,20 @@ module RubyLLM
71
71
  raise ArgumentError, 'Vertex AI research requires the supported Deep Research agent ID'
72
72
  end
73
73
 
74
- def render_research_payload(prompt, agent:, with:, server_tools:, provider_options:)
74
+ def render_research_payload(prompt, agent:, with:, provider_tools:, provider_options:)
75
75
  raise ArgumentError, 'Research requires a nonempty prompt' unless prompt.is_a?(String) && !prompt.empty?
76
76
 
77
77
  attachments = Attachment.wrap(with, config: @config)
78
78
  validate_research_attachments(attachments)
79
79
  payload = { agent:, input: render_interaction_content(prompt, attachments), background: true, stream: false }
80
- payload[:tools] = [] unless server_tools.nil?
80
+ payload[:tools] = [] unless provider_tools.nil?
81
81
  options = render_research_options(provider_options)
82
- entries = if server_tools.is_a?(Hash)
83
- RubyLLM::Tools::ServerTools.normalize([], server_tools)
82
+ entries = if provider_tools.is_a?(Hash)
83
+ RubyLLM::Tools::ProviderTools.normalize([], provider_tools)
84
84
  else
85
- RubyLLM::Tools::ServerTools.normalize(Array(server_tools), {})
85
+ RubyLLM::Tools::ProviderTools.normalize(Array(provider_tools), {})
86
86
  end
87
- apply_server_tools(payload, entries).merge(options)
87
+ apply_provider_tools(payload, entries).merge(options)
88
88
  end
89
89
 
90
90
  def validate_research_attachments(attachments)
@@ -130,13 +130,13 @@ module RubyLLM
130
130
 
131
131
  def complete(messages, tools:, temperature:, model:, provider_options: {}, headers: {}, schema: nil, # :nodoc:
132
132
  max_output_tokens: nil, thinking: nil, citations: false, caching: nil, tool_prefs: nil,
133
- protocol: nil, before_request: [], usage_recorder: nil, server_tools: [],
133
+ protocol: nil, before_request: [], usage_recorder: nil, provider_tools: [],
134
134
  compaction: nil, end_user: nil, &)
135
135
  protocol_class = resolve_protocol(protocol, model, tools:, schema:, thinking:, tool_prefs:, citations:)
136
136
  protocol_class.new(self, model).complete(
137
137
  messages,
138
138
  tools: tools,
139
- server_tools: server_tools,
139
+ provider_tools: provider_tools,
140
140
  tool_prefs: tool_prefs,
141
141
  temperature: temperature,
142
142
  max_output_tokens: max_output_tokens,
@@ -166,12 +166,12 @@ module RubyLLM
166
166
 
167
167
  def render(messages, tools:, temperature:, model:, provider_options: {}, schema: nil, thinking: nil, # :nodoc:
168
168
  max_output_tokens: nil, citations: false, caching: nil, tool_prefs: nil, protocol: nil,
169
- before_request: [], server_tools: [], compaction: nil, end_user: nil)
169
+ before_request: [], provider_tools: [], compaction: nil, end_user: nil)
170
170
  protocol_class = resolve_protocol(protocol, model, tools:, schema:, thinking:, tool_prefs:, citations:)
171
171
  protocol_class.new(self, model).render(
172
172
  messages,
173
173
  tools: tools,
174
- server_tools: server_tools,
174
+ provider_tools: provider_tools,
175
175
  tool_prefs: tool_prefs,
176
176
  temperature: temperature,
177
177
  max_output_tokens: max_output_tokens,
@@ -44,6 +44,10 @@ module RubyLLM
44
44
  payload = super
45
45
  payload.delete(:reasoning_effort)
46
46
  strip_schema_strict(payload)
47
+ if tool_prefs&.dig(:choice) == :none
48
+ payload.delete(:tools)
49
+ payload.delete(:parallel_tool_calls)
50
+ end
47
51
 
48
52
  reasoning = build_reasoning(thinking)
49
53
  payload[:reasoning] = reasoning if reasoning
@@ -67,15 +67,15 @@ module RubyLLM
67
67
 
68
68
  # Submits one research task without waiting. +provider:+ and +agent:+
69
69
  # are required; +with:+ attaches documents or images where supported.
70
- # +server_tools:+ accepts an array of aliases or a Hash of aliases
71
- # and their options, as on Chat#with_server_tools.
72
- def self.research_later(prompt, provider:, agent:, with: nil, server_tools: nil,
70
+ # +provider_tools:+ accepts an array of aliases or a Hash of aliases
71
+ # and their options, as on Chat#with_provider_tools.
72
+ def self.research_later(prompt, provider:, agent:, with: nil, provider_tools: nil,
73
73
  context: nil, provider_options: {}, metadata: nil)
74
74
  config = context&.config || RubyLLM.config
75
75
  instance = Provider.resolve!(provider).new(config)
76
76
  payload = { provider: instance.slug, agent:, prompt:, metadata: }
77
77
  RubyLLM.instrument('research_job.ruby_llm', payload, config:) do |event|
78
- job = instance.research_later(prompt, agent:, with:, server_tools:, provider_options:)
78
+ job = instance.research_later(prompt, agent:, with:, provider_tools:, provider_options:)
79
79
  event[:job_id] = job.id
80
80
  event[:status] = job.status
81
81
  job
@@ -5,9 +5,9 @@ module RubyLLM
5
5
  # response: a web search the model ran, a code execution, or the results
6
6
  # block a provider returned for one. They appear on
7
7
  # Message#server_tool_calls when a chat enables tools with
8
- # Chat#with_server_tools.
8
+ # Chat#with_provider_tools.
9
9
  #
10
- # response = chat.with_server_tools(:web_search).ask "What changed in Ruby 3.5?"
10
+ # response = chat.with_provider_tools(:web_search).ask "What changed in Ruby 3.5?"
11
11
  # response.server_tool_calls.map(&:type) # => ["server_tool_use", "web_search_tool_result"]
12
12
  #
13
13
  # RubyLLM does not model each tool's result schema. #raw always holds the
@@ -2,12 +2,12 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Tools # :nodoc:
5
- # Normalizes and resolves the server tools a chat enabled with
6
- # Chat#with_server_tools. Protocols declare an alias table mapping portable
5
+ # Normalizes and resolves the provider tools a chat enabled with
6
+ # Chat#with_provider_tools. Protocols declare an alias table mapping portable
7
7
  # names such as +:web_search+ to their wire format; raw Hashes pass through
8
8
  # verbatim so new provider tools work without a gem update. :nodoc:
9
- module ServerTools # :nodoc: all
10
- # The result of resolving a chat's server tools against a protocol's
9
+ module ProviderTools # :nodoc: all
10
+ # The result of resolving a chat's provider tools against a protocol's
11
11
  # alias table: tool entries for the payload's tools slot, extra payload
12
12
  # fields to merge, and request headers to add.
13
13
  class Resolution
@@ -61,7 +61,7 @@ module RubyLLM
61
61
 
62
62
  module_function
63
63
 
64
- # Normalizes with_server_tools arguments into entry hashes:
64
+ # Normalizes with_provider_tools arguments into entry hashes:
65
65
  # {name:, options:} for aliases and {raw:} for verbatim tool specs.
66
66
  def normalize(tools, tools_with_options)
67
67
  entries = tools.compact.map { |tool| normalize_positional(tool) }
@@ -74,13 +74,13 @@ module RubyLLM
74
74
  when Hash then normalized_entry?(tool) ? tool : { raw: tool }
75
75
  else
76
76
  raise ArgumentError,
77
- "Server tools must be Symbols or Hashes, got #{tool.class}. " \
77
+ "Provider tools must be Symbols or Hashes, got #{tool.class}. " \
78
78
  'Pass :web_search for a portable alias or the provider\'s tool definition as a Hash.'
79
79
  end
80
80
  end
81
81
 
82
82
  # Entries that already went through normalize (an Agent's declared
83
- # server_tools, for example) pass through unchanged.
83
+ # provider_tools, for example) pass through unchanged.
84
84
  def normalized_entry?(hash)
85
85
  hash.key?(:raw) ? hash.size == 1 : hash.size == 2 && hash.key?(:name) && hash.key?(:options)
86
86
  end
@@ -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.rc4'
5
+ VERSION = '2.0.0'
6
6
  end
data/lib/ruby_llm.rb CHANGED
@@ -75,7 +75,7 @@ loader.setup
75
75
  #
76
76
  # Subclass Tool and implement +execute+ to give the model an application
77
77
  # action. Tool.requires_approval pauses execution for a human decision;
78
- # Chat#approve and Chat#deny record it. Chat#with_server_tools enables
78
+ # Chat#approve and Chat#deny record it. Chat#with_provider_tools enables
79
79
  # provider-executed tools such as web search, code execution, and remote MCP.
80
80
  # Their calls appear as ServerToolCall values, with Citation values for sources.
81
81
  #
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.rc4
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino
@@ -589,7 +589,7 @@ files:
589
589
  - lib/ruby_llm/tokens.rb
590
590
  - lib/ruby_llm/tool.rb
591
591
  - lib/ruby_llm/tool_call.rb
592
- - lib/ruby_llm/tools/server_tools.rb
592
+ - lib/ruby_llm/tools/provider_tools.rb
593
593
  - lib/ruby_llm/transcription.rb
594
594
  - lib/ruby_llm/transcription/wav_audio.rb
595
595
  - lib/ruby_llm/transcription_chunk.rb
@@ -617,7 +617,7 @@ metadata:
617
617
  funding_uri: https://github.com/sponsors/crmne
618
618
  rubygems_mfa_required: 'true'
619
619
  post_install_message: |
620
- RubyLLM 2.0.0.rc4
620
+ RubyLLM 2.0.0
621
621
 
622
622
  2.0 renames several APIs and changes what message content returns. Coming
623
623
  from 1.x? Read the upgrade guide before you boot: