ruby_llm-responses_api 0.6.0 → 1.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.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/MIGRATION.md +276 -0
  4. data/README.md +42 -32
  5. data/lib/ruby_llm/providers/openai_responses/active_record_extension.rb +12 -109
  6. data/lib/ruby_llm/providers/openai_responses/base.rb +2 -7
  7. data/lib/ruby_llm/providers/openai_responses/batch.rb +8 -8
  8. data/lib/ruby_llm/providers/openai_responses/batches.rb +6 -14
  9. data/lib/ruby_llm/providers/openai_responses/built_in_tools.rb +1 -1
  10. data/lib/ruby_llm/providers/openai_responses/chat_extension.rb +3 -12
  11. data/lib/ruby_llm/providers/openai_responses/compaction.rb +1 -1
  12. data/lib/ruby_llm/providers/openai_responses/message_extension.rb +11 -21
  13. data/lib/ruby_llm/providers/openai_responses/model_registry.rb +17 -261
  14. data/lib/ruby_llm/providers/openai_responses/protocol.rb +103 -0
  15. data/lib/ruby_llm/providers/openai_responses/stream_accumulator.rb +48 -0
  16. data/lib/ruby_llm/providers/openai_responses/web_socket.rb +51 -77
  17. data/lib/ruby_llm/providers/openai_responses.rb +29 -65
  18. data/lib/rubyllm_responses_api.rb +13 -46
  19. metadata +17 -18
  20. data/lib/ruby_llm/providers/openai_responses/capabilities.rb +0 -263
  21. data/lib/ruby_llm/providers/openai_responses/chat.rb +0 -273
  22. data/lib/ruby_llm/providers/openai_responses/media.rb +0 -95
  23. data/lib/ruby_llm/providers/openai_responses/models.rb +0 -48
  24. data/lib/ruby_llm/providers/openai_responses/stream_accumulator_extension.rb +0 -39
  25. data/lib/ruby_llm/providers/openai_responses/streaming.rb +0 -137
  26. data/lib/ruby_llm/providers/openai_responses/tools.rb +0 -222
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: daf5eec383489e81c577b2f04965ca156b976ca30a708a39a6ab8dce39863ff7
4
- data.tar.gz: df8e9a6f230724e40f479679229d20c1b2faeee8e186400f37367e059badea8a
3
+ metadata.gz: 6b169a57a059b3f091ea37d5f46dea3f253acb25f3544ad5f98ddec9f81e0185
4
+ data.tar.gz: f5251e33d031cfb749454c344d4e88e0edd83940dced27770d0a29d2de0b71cf
5
5
  SHA512:
6
- metadata.gz: 7b6ae940cd27e283b2fd06edcb9122fb25328f39d786f3c011073954a4e5574a63f4272b17e11816e499c3064343407181a044768ebaa7f4f9d1844ea3afae3b
7
- data.tar.gz: e2f971bfe9d6d874e6b368775485dab455ce16a0291c11d1806adf279273c3b6e762fac015ab86d376d84c3d31e0e1de26475ec5230c912d7352ad0b29c0a7f1
6
+ metadata.gz: b53e038d5e2c6d022c827a674ae5c14c7fb346922ce6b63d38d0b11755ac291ab3d4515ab66214a9fb65e34b18c544b26ea5031bffac41957e0d04e2c43a37db
7
+ data.tar.gz: 5974b3b5934dbf0fd9f7d4a5caddc6ec265b8cf3b899a65fdcaf372c3962daad092ca71ad6a8ae2bbda238b062ac0f9559e53151c7be6fcf0f73ba4e1d592c2e
data/CHANGELOG.md CHANGED
@@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.0] - 2026-10-31
9
+
10
+ ### Changed
11
+
12
+ - Migrate to RubyLLM 2.1.1's native provider/protocol architecture; require Ruby 4.x and RubyLLM 2.1.x.
13
+ - Keep `:openai_responses` as an opt-in OpenAI provider with a scoped native Responses protocol. Native `:openai` and other providers are unchanged.
14
+ - Delegate message rendering, attachments, function tools, reasoning, citations, server tool records, batches and usage accounting to RubyLLM; reuse its OpenAI model catalog instead of maintaining snapshots.
15
+ - Move raw-input batch factories to `RubyLLM::ResponsesAPI.batch` / `.batches`, preserving native `RubyLLM.batch(chats)`.
16
+ - Use RubyLLM 2.x's `with_provider_options`, `before_tool_call`, `after_tool_result`, `Message#tokens` and `Message#model` APIs. See MIGRATION.md.
17
+ - Extend native ActiveRecord serialization and message attributes without replacing persistence of usage, tool records, reasoning or attachments.
18
+
19
+ ### Added
20
+
21
+ - Completed streams expose the final Responses object at `message.raw.body`, including built-in tool output, matching sync calls (issue #4).
22
+ - Explicit stateless mode with `store: false`, without automatic chaining or discarded local history.
23
+ - Native server tool records and usage accounting for WebSocket chat requests.
24
+
25
+ ### Fixed
26
+
27
+ - Fail on a WebSocket disconnect before completion instead of returning a partial answer as a successful response.
28
+ - Preserve an existing in-flight WebSocket request when a concurrent call is rejected.
29
+ - Use native instrumented DELETE transport for response and container lifecycle operations.
30
+
31
+ - `StreamAccumulator` now forwards `response_id` from the completed chunk to the assembled message, matching the existing `built_in_tool_events` handling. Previously every streamed chat lost `response_id`, so `previous_response_id` chaining never engaged and each turn resent the full message history instead of just the new items.
32
+
33
+ ## [0.6.1] - 2026-06-11
34
+
35
+ ### Added
36
+
37
+ - RubyLLM 1.16 compatibility for request instrumentation, including `request.ruby_llm` events for manual Responses API DELETE requests while preserving RubyLLM 1.15 compatibility
38
+ - Explicit provider `configuration_options` for OpenAI API key, base URL, organization, and project settings
39
+ - Explicit `supports_tool_parallel_control?` capability so RubyLLM can discover Responses API support for `calls: :many` / `calls: :one`
40
+
41
+ ### Fixed
42
+
43
+ - Streamed function-call argument accumulation with RubyLLM 1.16's indexed `StreamAccumulator`, including Responses API `item_id` deltas
44
+
8
45
  ## [0.6.0] - 2026-05-26
9
46
 
10
47
  ### Added
data/MIGRATION.md ADDED
@@ -0,0 +1,276 @@
1
+ # Migrating from 0.x to 1.0
2
+
3
+ Version 1.0 uses RubyLLM's native OpenAI Responses protocol and adds server-side conversation state, WebSocket transport, containers and response lifecycle helpers. Keep `provider: :openai_responses` to retain this gem's behavior.
4
+
5
+ This guide covers application changes from `ruby_llm-responses_api` 0.x to 1.0.
6
+
7
+ ## 1. Upgrade the runtime and dependencies
8
+
9
+ The supported versions are:
10
+
11
+ | Dependency | Requirement |
12
+ | --- | --- |
13
+ | Ruby | 4.x only: `>= 4.0, < 5.0` |
14
+ | RubyLLM | `>= 2.1.1, < 2.2` |
15
+ | ruby_llm-responses_api | 1.0.x |
16
+ | websocket-client-simple | Optional; required for WebSocket transport |
17
+
18
+ For Rails applications still on RubyLLM 1.x, first follow the upstream [1.x to 2.0 guide](https://github.com/crmne/ruby_llm/blob/v2.0.0/docs/_reference/upgrading.md), then the [2.0 to 2.1 guide](https://rubyllm.com/upgrading/). Complete the database migration and cleanup steps for each release before moving to the next. The upstream 2.1 upgrade generator does not include the earlier upgrade helpers. This gem's 0.x implementation cannot be loaded alongside RubyLLM 2.x; account for that in the intermediate upgrade environment.
19
+
20
+ Set Ruby 4 in your runtime configuration, container image and CI. For mise:
21
+
22
+ ```toml
23
+ [tools]
24
+ ruby = "4.0"
25
+ ```
26
+
27
+ Use these final dependency constraints in your application's `Gemfile`:
28
+
29
+ ```ruby
30
+ ruby '>= 4.0', '< 5.0'
31
+
32
+ gem 'ruby_llm', '~> 2.1.1'
33
+ gem 'ruby_llm-responses_api', '~> 1.0.0'
34
+
35
+ # Add when using WebSocket transport:
36
+ gem 'websocket-client-simple', '~> 0.8'
37
+ ```
38
+
39
+ Then resolve the new bundle:
40
+
41
+ ```sh
42
+ bundle update ruby_llm ruby_llm-responses_api
43
+ ```
44
+
45
+ ## 2. Keep the provider and configuration
46
+
47
+ The require path, OpenAI credentials and provider name stay the same:
48
+
49
+ ```ruby
50
+ require 'ruby_llm-responses_api'
51
+
52
+ RubyLLM.configure do |config|
53
+ config.openai_api_key = ENV.fetch('OPENAI_API_KEY')
54
+ end
55
+
56
+ chat = RubyLLM.chat(model: 'gpt-4o', provider: :openai_responses)
57
+ response = chat.ask('Hello!')
58
+ puts response.content
59
+ ```
60
+
61
+ Keep existing `openai_responses` provider values in persisted chats and model records. Switching to `provider: :openai` selects RubyLLM's native stateless behavior and does not enable this gem's automatic chaining or transport helpers.
62
+
63
+ Model capabilities, limits and pricing now come from the native OpenAI catalog. For a model absent from that catalog, pass `assume_model_exists: true` with an explicit provider. Remove application code that calls the old `ModelRegistry.register_all!`.
64
+
65
+ ## 3. Update chat and message APIs
66
+
67
+ | Before | After |
68
+ | --- | --- |
69
+ | `chat.with_params(...)` | `chat.with_provider_options(...)` |
70
+ | `chat.with_tool(MyTool)` | `chat.with_tools(MyTool)` |
71
+ | `chat.on_tool_call { ... }` | `chat.before_tool_call { ... }` |
72
+ | `chat.on_tool_result { ... }` | `chat.after_tool_result { ... }` |
73
+ | `response.input_tokens` | `response.tokens.input` |
74
+ | `response.output_tokens` | `response.tokens.output` |
75
+ | `response.cached_tokens` | `response.tokens.cache_read` |
76
+ | `response.cache_creation_tokens` | `response.tokens.cache_write` |
77
+ | `response.model_id` | `response.model` |
78
+ | Structured `response.content` | `response.parsed` |
79
+ | `RubyLLM::Content` attachments | `chat.ask('Describe', with: files_or_urls)` |
80
+ | `Chat.create!(model_id: ..., provider: ...)` | `Chat.create!(model: ..., provider: ...)` |
81
+
82
+ `response.content` is a string even when a schema is configured. Read structured data through `response.parsed`:
83
+
84
+ ```ruby
85
+ chat.with_schema(
86
+ type: 'object',
87
+ properties: { answer: { type: 'string' } },
88
+ required: ['answer'],
89
+ additionalProperties: false
90
+ )
91
+ response = chat.ask('Give a short answer.')
92
+ puts response.parsed.fetch('answer')
93
+ ```
94
+
95
+ Pass attachments directly to `ask`:
96
+
97
+ ```ruby
98
+ chat.ask('Summarize this document.', with: ['report.pdf'])
99
+ chat.ask('Describe this image.', with: ['photo.png'])
100
+ ```
101
+
102
+ `with_provider_options` replaces the current options hash. Preserve other options when changing one:
103
+
104
+ ```ruby
105
+ chat.with_provider_options(chat.provider_options.merge(store: false))
106
+ ```
107
+
108
+ ## 4. Check conversation state
109
+
110
+ Automatic chaining remains the default for `:openai_responses`. Requests use `store: true`; the next request sends only messages after the last stored assistant response, with its ID in `previous_response_id`. Current system instructions are sent again on each request. This applies to sync HTTP, HTTP streaming and WebSocket calls.
111
+
112
+ ```ruby
113
+ chat = RubyLLM.chat(model: 'gpt-4o', provider: :openai_responses)
114
+ first = chat.ask('My name is Alice.')
115
+ puts first.response_id
116
+ chat.ask('What is my name?')
117
+ ```
118
+
119
+ Remove workarounds that manually trim history or patch `format_input` for issue #10. History filtering is handled by the new protocol. Keep complete local conversation history so stateless replay remains possible.
120
+
121
+ To disable automatic chaining and replay the local history:
122
+
123
+ ```ruby
124
+ chat.with_provider_options(store: false)
125
+ response = chat.ask('Continue using the local history.')
126
+ ```
127
+
128
+ In this mode, returned messages have no automatic chaining `response_id`. The native protocol preserves reasoning data for replay. If needed for diagnostics, the API's returned ID is available at `response.raw.body['id']`.
129
+
130
+ To start a fresh stored chain from the full local history, explicitly set `previous_response_id: nil` for that request. Remove the override before the next request:
131
+
132
+ ```ruby
133
+ options = chat.provider_options.except(:previous_response_id).merge(store: true)
134
+ chat.with_provider_options(options.merge(previous_response_id: nil))
135
+ chat.ask('Start a fresh chain using this history.')
136
+ chat.with_provider_options(options)
137
+ ```
138
+
139
+ An explicit external `previous_response_id` can resume a stored response in a new chat. Supply only the new local input for that parent; do not also import its entire old history.
140
+
141
+ ## 5. Update tools and callbacks
142
+
143
+ Custom function tools use RubyLLM's native declarations:
144
+
145
+ ```ruby
146
+ class Weather < RubyLLM::Tool
147
+ description 'Get the weather for a city'
148
+ parameter :city, type: 'string', description: 'City name'
149
+
150
+ def execute(city:)
151
+ "Sunny in #{city}"
152
+ end
153
+ end
154
+
155
+ chat.with_tools(Weather)
156
+ ```
157
+
158
+ Replace the old `param` declaration with `parameter`, and `desc:` with `description:`. Native function definitions default to non-strict mode. To retain strict definitions, add `provider_options strict: true` to the tool class.
159
+
160
+ Built-in tool hash builders remain available:
161
+
162
+ ```ruby
163
+ tool = RubyLLM::ResponsesAPI::BuiltInTools.shell
164
+ chat.with_provider_options(tools: [tool])
165
+ ```
166
+
167
+ For tools supported by RubyLLM's native helpers, you can also use:
168
+
169
+ ```ruby
170
+ chat.with_provider_tools(:web_search, :code_execution)
171
+ ```
172
+
173
+ Rename the callbacks:
174
+
175
+ ```ruby
176
+ chat.before_tool_call { |call| puts "#{call.name}: #{call.id}" }
177
+ chat.after_tool_result { |result| puts result.inspect }
178
+ ```
179
+
180
+ For built-in server tools, callbacks run when the final assistant message is added. They report completed work, not live progress, and retain this gem's result hash format. Local function tools keep RubyLLM's own callback arguments.
181
+
182
+ Use `response.server_tool_calls` for native server tool records. Each record's `raw` retains its provider-specific fields. Existing parsers such as `BuiltInTools.parse_shell_call_results(response.raw.body['output'])` remain available.
183
+
184
+ For this provider, `response.raw.body` contains the final Responses object in all three transports. For streaming, this is a reconstructed body from the final response event, rather than the SSE wire body. Remove old patches that reconstruct built-in tool metadata or forward `response_id` through the global accumulator.
185
+
186
+ ## 6. Move raw-input batch calls to ResponsesAPI
187
+
188
+ The gem no longer overrides `RubyLLM.batch`. Rename the factories used by the existing raw-input workflow:
189
+
190
+ | Before | After |
191
+ | --- | --- |
192
+ | `RubyLLM.batch(model: ..., provider: ...)` | `RubyLLM::ResponsesAPI.batch(model: ..., provider: ...)` |
193
+ | `RubyLLM.batch(id: ...)` | `RubyLLM::ResponsesAPI.batch(id: ...)` |
194
+ | `RubyLLM.batches(...)` | `RubyLLM::ResponsesAPI.batches(...)` |
195
+
196
+ ```ruby
197
+ batch = RubyLLM::ResponsesAPI.batch(model: 'gpt-4o')
198
+ batch.add('What is Ruby?', id: 'ruby')
199
+ batch.create!
200
+ batch.wait!
201
+ puts batch.results.fetch('ruby').content
202
+
203
+ # Resume or list existing batches:
204
+ RubyLLM::ResponsesAPI.batch(id: 'batch_existing')
205
+ RubyLLM::ResponsesAPI.batches
206
+ ```
207
+
208
+ The helper defaults to `provider: :openai_responses`. RubyLLM's separate native `RubyLLM.batch(chats)` API remains available for its persisted-chat batch workflow.
209
+
210
+ ## 7. Retain Rails response IDs
211
+
212
+ Complete RubyLLM's own schema upgrades first. Keep the string `response_id` column on your application message table. If it is already present, this gem needs no additional schema change. If it is absent:
213
+
214
+ ```sh
215
+ bin/rails generate migration AddResponseIdToMessages response_id:string
216
+ bin/rails db:migrate
217
+ ```
218
+
219
+ Substitute your actual message table name when using a custom model. The column is optional, but automatic chaining cannot resume across a database reload without it.
220
+
221
+ Update chat creation to use `model:`:
222
+
223
+ ```ruby
224
+ chat = Chat.create!(model: 'gpt-4o', provider: :openai_responses)
225
+ chat.ask('My name is Alice.')
226
+
227
+ # After a restart:
228
+ Chat.find(chat.id).ask('What is my name?')
229
+ ```
230
+
231
+ The extension preserves native persistence of usage, reasoning, citations, attachments and tool records, while adding `response_id`. If using ActiveRecord outside Rails, apply the extension after loading ActiveRecord:
232
+
233
+ ```ruby
234
+ require 'active_record'
235
+ RubyLLM::Providers::OpenAIResponses.apply_active_record_extensions!
236
+ ```
237
+
238
+ ## 8. Update WebSocket options and error handling
239
+
240
+ Add `websocket-client-simple` to your application's bundle, then rename `with_params`:
241
+
242
+ ```ruby
243
+ chat.with_provider_options(transport: :websocket)
244
+ chat.ask('Hello') { |chunk| print chunk.content }
245
+ ```
246
+
247
+ Additional chat headers are sent in the handshake. Changing them reconnects the transport. A disconnect before completion now raises `RubyLLM::ResponsesAPI::WebSocket::ConnectionError` instead of returning a partial answer as a successful response. Handle that exception in the application's transport failure path.
248
+
249
+ Standalone `RubyLLM::ResponsesAPI::WebSocket` remains available. Its `tools:` argument accepts raw Responses API tool hashes; use `BuiltInTools` to build those hashes.
250
+
251
+ ## 9. Remove patches against retired internals
252
+
253
+ These helpers remain available under `RubyLLM::ResponsesAPI`: `Batch`, `Batches`, `BuiltInTools`, `State`, `Background`, `Compaction`, `Containers` and `WebSocket`.
254
+
255
+ The old provider `Chat`, `Streaming`, `Media`, `Models`, `Tools` and `Capabilities` modules have been removed. Update direct requires and monkey patches against those modules. Rendering and parsing now use the native RubyLLM protocol; this gem's accumulator is scoped to its own provider.
256
+
257
+ ## Verify the application upgrade
258
+
259
+ Before deploying, check the application paths you use:
260
+
261
+ - A sync and a streamed two-turn chat retain `response_id` and send only new input on the second turn.
262
+ - A tool call executes and its result reaches the follow-up request.
263
+ - Structured output uses `response.parsed`; token displays use `response.tokens`.
264
+ - A persisted chat can resume after a fresh database load.
265
+ - Raw-input batch calls use `RubyLLM::ResponsesAPI`.
266
+ - WebSocket disconnects reach the application's failure handler.
267
+
268
+ For contributors, `bundle exec rake` runs this repository's tests and RuboCop on Ruby 4. Historic VCR recordings remain unchanged and use a compatibility matcher for equivalent 0.x request shapes. Separate WebMock tests assert the new payload, state, persistence and WebSocket behavior.
269
+
270
+ The optional live WebSocket check sends real requests using the configured OpenAI account:
271
+
272
+ ```sh
273
+ bundle exec rspec spec/integration/websocket_spec.rb --tag live_ws
274
+ ```
275
+
276
+ Set `OPENAI_API_KEY` before running that check.
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # RubyLLM Responses API
2
2
 
3
- A [RubyLLM](https://github.com/crmne/ruby_llm) provider for OpenAI's [Responses API](https://platform.openai.com/docs/api-reference/responses).
3
+ Stateful conversations, WebSocket transport, containers and response lifecycle helpers on top of [RubyLLM](https://github.com/crmne/ruby_llm)'s native OpenAI Responses protocol.
4
+
5
+ Version 1.0 requires Ruby 4.x and RubyLLM 2.1.1 (2.1.x). Upgrading from 0.x? See [MIGRATION.md](MIGRATION.md).
6
+
7
+ `provider: :openai_responses` opts into these extensions. RubyLLM's `provider: :openai`, other providers, and native `RubyLLM.batch(chats)` keep their normal behavior. Standard rendering, parsing, model metadata, reasoning, citations, tool execution and usage accounting come from RubyLLM.
4
8
 
5
9
  ## Installation
6
10
 
@@ -26,7 +30,7 @@ All standard RubyLLM features work as expected (streaming, tools, vision, struct
26
30
 
27
31
  ## Stateful Conversations
28
32
 
29
- Conversations automatically chain via `previous_response_id`:
33
+ Conversations automatically chain via `previous_response_id`, with `store: true`. Only messages after the last stored assistant response are sent; current system instructions are sent on every turn. This works with HTTP, streaming and WebSocket transport:
30
34
 
31
35
  ```ruby
32
36
  chat = RubyLLM.chat(model: 'gpt-5.5', provider: :openai_responses)
@@ -34,9 +38,11 @@ chat.ask("My name is Alice.")
34
38
  chat.ask("What's my name?") # => "Your name is Alice."
35
39
  ```
36
40
 
41
+ To use stateless replay instead, set `chat.with_provider_options(store: false)`. The full history (including native reasoning data) is sent and no response ID is retained for automatic chaining. To start a fresh chain from the local history while still storing the new response, set `previous_response_id: nil`; remove that override before resuming automatic chaining.
42
+
37
43
  ## Rails Persistence
38
44
 
39
- For conversations that survive app restarts, add a migration:
45
+ First complete RubyLLM's own 2.x database migration. For conversations that survive app restarts, retain or add a `response_id` column on your application message model:
40
46
 
41
47
  ```ruby
42
48
  class AddResponseIdToMessages < ActiveRecord::Migration[7.0]
@@ -50,7 +56,7 @@ Then use normally:
50
56
 
51
57
  ```ruby
52
58
  # Day 1
53
- chat = Chat.create!(model_id: 'gpt-5.5', provider: :openai_responses)
59
+ chat = Chat.create!(model: 'gpt-5.5', provider: :openai_responses)
54
60
  chat.ask("My name is Alice.")
55
61
 
56
62
  # Day 2 (after restart)
@@ -60,17 +66,17 @@ chat.ask("What's my name?") # => "Alice"
60
66
 
61
67
  ## Built-in Tools
62
68
 
63
- The Responses API provides built-in tools that don't require custom implementation. Pass them as hashes via `with_params`, or use the `BuiltInTools` helper module.
69
+ The Responses API provides built-in tools that don't require custom implementation. Pass them as hashes via `with_provider_options`, or use the `BuiltInTools` helper module.
64
70
 
65
71
  ### Web Search
66
72
 
67
73
  ```ruby
68
- chat.with_params(tools: [{ type: 'web_search' }])
74
+ chat.with_provider_options(tools: [{ type: 'web_search' }])
69
75
  chat.ask("Latest news about Ruby 3.4?")
70
76
 
71
77
  # Or with helper
72
78
  tool = RubyLLM::ResponsesAPI::BuiltInTools.web_search(search_context_size: 'high')
73
- chat.with_params(tools: [tool])
79
+ chat.with_provider_options(tools: [tool])
74
80
 
75
81
  # Legacy preview type is still available when needed
76
82
  tool = RubyLLM::ResponsesAPI::BuiltInTools.web_search_preview
@@ -81,7 +87,7 @@ tool = RubyLLM::ResponsesAPI::BuiltInTools.web_search_preview
81
87
  Execute Python code in a sandbox:
82
88
 
83
89
  ```ruby
84
- chat.with_params(tools: [{ type: 'code_interpreter' }])
90
+ chat.with_provider_tools(:code_execution)
85
91
  chat.ask("Calculate the first 20 Fibonacci numbers and plot them")
86
92
  ```
87
93
 
@@ -90,7 +96,7 @@ chat.ask("Calculate the first 20 Fibonacci numbers and plot them")
90
96
  Search through uploaded files (requires vector store setup):
91
97
 
92
98
  ```ruby
93
- chat.with_params(tools: [{ type: 'file_search', vector_store_ids: ['vs_abc123'] }])
99
+ chat.with_provider_options(tools: [{ type: 'file_search', vector_store_ids: ['vs_abc123'] }])
94
100
  chat.ask("What does the documentation say about authentication?")
95
101
  ```
96
102
 
@@ -101,12 +107,12 @@ Execute commands in hosted containers or local terminal environments. Requires G
101
107
  ```ruby
102
108
  # Auto-provisioned container (default)
103
109
  chat = RubyLLM.chat(model: 'gpt-5.5', provider: :openai_responses)
104
- chat.with_params(tools: [{ type: 'shell', environment: { type: 'container_auto' } }])
110
+ chat.with_provider_options(tools: [{ type: 'shell', environment: { type: 'container_auto' } }])
105
111
  chat.ask("List all Python files in the project")
106
112
 
107
113
  # Using helper
108
114
  tool = RubyLLM::ResponsesAPI::BuiltInTools.shell
109
- chat.with_params(tools: [tool])
115
+ chat.with_provider_options(tools: [tool])
110
116
 
111
117
  # Reuse an existing container
112
118
  tool = RubyLLM::ResponsesAPI::BuiltInTools.shell(container_id: 'cntr_abc123')
@@ -135,18 +141,18 @@ Structured diff-based file editing. Requires GPT-5 family models.
135
141
 
136
142
  ```ruby
137
143
  chat = RubyLLM.chat(model: 'gpt-5.5', provider: :openai_responses)
138
- chat.with_params(tools: [{ type: 'apply_patch' }])
144
+ chat.with_provider_options(tools: [{ type: 'apply_patch' }])
139
145
  chat.ask("Add error handling to the User#save method")
140
146
 
141
147
  # Using helper
142
148
  tool = RubyLLM::ResponsesAPI::BuiltInTools.apply_patch
143
- chat.with_params(tools: [tool])
149
+ chat.with_provider_options(tools: [tool])
144
150
  ```
145
151
 
146
152
  ### Image Generation
147
153
 
148
154
  ```ruby
149
- chat.with_params(tools: [{ type: 'image_generation' }])
155
+ chat.with_provider_options(tools: [{ type: 'image_generation' }])
150
156
  chat.ask("Generate an image of a sunset over mountains")
151
157
  ```
152
158
 
@@ -158,13 +164,13 @@ tool = RubyLLM::ResponsesAPI::BuiltInTools.mcp(
158
164
  server_url: 'https://api.github.com/mcp',
159
165
  require_approval: 'never'
160
166
  )
161
- chat.with_params(tools: [tool])
167
+ chat.with_provider_options(tools: [tool])
162
168
  ```
163
169
 
164
170
  ### Combining Tools
165
171
 
166
172
  ```ruby
167
- chat.with_params(tools: [
173
+ chat.with_provider_options(tools: [
168
174
  { type: 'web_search' },
169
175
  { type: 'code_interpreter' },
170
176
  { type: 'shell', environment: { type: 'container_auto' } }
@@ -180,12 +186,12 @@ For multi-hour agent runs, enable server-side compaction to automatically compre
180
186
  chat = RubyLLM.chat(model: 'gpt-4o', provider: :openai_responses)
181
187
 
182
188
  # Pass directly
183
- chat.with_params(
189
+ chat.with_provider_options(
184
190
  context_management: [{ type: 'compaction', compact_threshold: 200_000 }]
185
191
  )
186
192
 
187
193
  # Or use the helper
188
- chat.with_params(
194
+ chat.with_provider_options(
189
195
  **RubyLLM::ResponsesAPI::Compaction.compaction_params(compact_threshold: 150_000)
190
196
  )
191
197
 
@@ -201,7 +207,7 @@ When the token count crosses the threshold, the server automatically compacts th
201
207
  You can also run an explicit compaction pass or count request input tokens before creating a response:
202
208
 
203
209
  ```ruby
204
- provider = chat.instance_variable_get(:@provider)
210
+ provider = chat.provider
205
211
 
206
212
  compacted = provider.compact_response(
207
213
  model: 'gpt-5.5',
@@ -221,7 +227,7 @@ Manage persistent execution environments for the shell tool and code interpreter
221
227
 
222
228
  ```ruby
223
229
  chat = RubyLLM.chat(model: 'gpt-5.5', provider: :openai_responses)
224
- provider = chat.instance_variable_get(:@provider)
230
+ provider = chat.provider
225
231
 
226
232
  # Create a container
227
233
  container = provider.create_container(
@@ -232,7 +238,7 @@ container = provider.create_container(
232
238
 
233
239
  # Use it with the shell tool
234
240
  tool = RubyLLM::ResponsesAPI::BuiltInTools.shell(container_id: container['id'])
235
- chat.with_params(tools: [tool])
241
+ chat.with_provider_options(tools: [tool])
236
242
  chat.ask("Install pandas and run my analysis script")
237
243
 
238
244
  # List files created in the container
@@ -251,11 +257,11 @@ For long-running tasks:
251
257
 
252
258
  ```ruby
253
259
  chat = RubyLLM.chat(model: 'gpt-4o', provider: :openai_responses)
254
- chat.with_params(background: true)
260
+ chat.with_provider_options(background: true)
255
261
  response = chat.ask("Analyze this large dataset...")
256
262
 
257
263
  # Poll for completion
258
- provider = chat.instance_variable_get(:@provider)
264
+ provider = chat.provider
259
265
  result = provider.poll_response(response.response_id, interval: 2.0) do |status|
260
266
  puts "Status: #{status['status']}"
261
267
  end
@@ -263,21 +269,21 @@ end
263
269
 
264
270
  ## Observing Built-in Tool Activity
265
271
 
266
- Server-side built-in tools (web search, code interpreter, file search, shell, apply patch, image generation, MCP, computer use, local shell) fire through the same `on_tool_call` / `on_tool_result` callbacks as locally executed function tools:
272
+ Server-side built-in tools (web search, code interpreter, file search, shell, apply patch, image generation, MCP, computer use, local shell) fire through the same `before_tool_call` / `after_tool_result` callbacks as locally executed function tools:
267
273
 
268
274
  ```ruby
269
275
  chat = RubyLLM.chat(model: 'gpt-4o', provider: :openai_responses)
270
- chat.with_params(tools: [{ type: 'web_search' }])
276
+ chat.with_provider_options(tools: [{ type: 'web_search' }])
271
277
 
272
- chat.on_tool_call { |tc| puts "#{tc.name} called (id=#{tc.id})" }
273
- chat.on_tool_result { |r| puts " -> status=#{r[:status]}" }
278
+ chat.before_tool_call { |tc| puts "#{tc.name} called (id=#{tc.id})" }
279
+ chat.after_tool_result { |r| puts " -> status=#{r[:status]}" }
274
280
 
275
281
  chat.ask("What's the latest Ruby release?")
276
282
  # web_search called (id=ws_...)
277
283
  # -> status=completed
278
284
  ```
279
285
 
280
- The newer `before_tool_call` / `after_tool_result` API (ruby_llm 1.13+) is supported too. Each `ToolCall` carries a normalized name (`web_search`, `code_interpreter`, `file_search`, `image_generation`, `shell`, `apply_patch`, `mcp`, `computer`, `local_shell`) and best-effort arguments extracted from the response item.
286
+ Each `ToolCall` carries a normalized name (`web_search`, `code_interpreter`, `file_search`, `image_generation`, `shell`, `apply_patch`, `mcp`, `computer`, `local_shell`) and best-effort arguments extracted from the response item.
281
287
 
282
288
  ## Parsing Built-in Tool Results
283
289
 
@@ -297,13 +303,17 @@ image_results = RubyLLM::ResponsesAPI::BuiltInTools.parse_image_generation_resu
297
303
  citations = RubyLLM::ResponsesAPI::BuiltInTools.extract_citations(message_content)
298
304
  ```
299
305
 
306
+ Completed built-in tool records are also available through native `response.server_tool_calls` (`call.raw` keeps the provider item). For this provider, completed HTTP streams and WebSocket calls expose the final Responses object at `response.raw.body`, matching sync responses. This streamed body is reconstructed from the final response event; it is not the SSE wire body.
307
+
300
308
  ## Batch API
301
309
 
310
+ RubyLLM 2.x has its own `RubyLLM.batch(chats)` API. The raw-input batch helper from this gem lives under `RubyLLM::ResponsesAPI` and does not override the native API.
311
+
302
312
  Process many requests asynchronously at 50% lower cost with a 24-hour completion window:
303
313
 
304
314
  ```ruby
305
315
  # Create a batch
306
- batch = RubyLLM.batch(model: 'gpt-4o', provider: :openai_responses)
316
+ batch = RubyLLM::ResponsesAPI.batch(model: 'gpt-4o', provider: :openai_responses)
307
317
 
308
318
  # Add requests (auto-generates IDs or use your own)
309
319
  batch.add("What is Ruby?")
@@ -323,14 +333,14 @@ results["request_0"].content # => "Ruby is a dynamic..."
323
333
  results["translate_1"].content # => "Hola"
324
334
 
325
335
  # Resume from a previous session
326
- batch = RubyLLM.batch(id: "batch_abc123", provider: :openai_responses)
336
+ batch = RubyLLM::ResponsesAPI.batch(id: "batch_abc123", provider: :openai_responses)
327
337
  batch.results
328
338
 
329
339
  # Cancel a running batch
330
340
  batch.cancel!
331
341
 
332
342
  # List existing batches
333
- RubyLLM.batches(provider: :openai_responses)
343
+ RubyLLM::ResponsesAPI.batches(provider: :openai_responses)
334
344
  ```
335
345
 
336
346
  **Constraints**: No `web_search`/`code_interpreter` tools, no `previous_response_id` chaining, max 50k requests per batch, 200MB file limit.
@@ -351,7 +361,7 @@ Just add `transport: :websocket` to your params -- the standard `chat.ask` API w
351
361
 
352
362
  ```ruby
353
363
  chat = RubyLLM.chat(model: 'gpt-4o', provider: :openai_responses)
354
- chat.with_params(transport: :websocket)
364
+ chat.with_provider_options(transport: :websocket)
355
365
 
356
366
  chat.ask("Hello!")
357
367
  chat.ask("What's 2+2?") # reuses the same WebSocket connection