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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/MIGRATION.md +276 -0
- data/README.md +42 -32
- data/lib/ruby_llm/providers/openai_responses/active_record_extension.rb +12 -109
- data/lib/ruby_llm/providers/openai_responses/base.rb +2 -7
- data/lib/ruby_llm/providers/openai_responses/batch.rb +8 -8
- data/lib/ruby_llm/providers/openai_responses/batches.rb +6 -14
- data/lib/ruby_llm/providers/openai_responses/built_in_tools.rb +1 -1
- data/lib/ruby_llm/providers/openai_responses/chat_extension.rb +3 -12
- data/lib/ruby_llm/providers/openai_responses/compaction.rb +1 -1
- data/lib/ruby_llm/providers/openai_responses/message_extension.rb +11 -21
- data/lib/ruby_llm/providers/openai_responses/model_registry.rb +17 -261
- data/lib/ruby_llm/providers/openai_responses/protocol.rb +103 -0
- data/lib/ruby_llm/providers/openai_responses/stream_accumulator.rb +48 -0
- data/lib/ruby_llm/providers/openai_responses/web_socket.rb +51 -77
- data/lib/ruby_llm/providers/openai_responses.rb +29 -65
- data/lib/rubyllm_responses_api.rb +13 -46
- metadata +17 -18
- data/lib/ruby_llm/providers/openai_responses/capabilities.rb +0 -263
- data/lib/ruby_llm/providers/openai_responses/chat.rb +0 -273
- data/lib/ruby_llm/providers/openai_responses/media.rb +0 -95
- data/lib/ruby_llm/providers/openai_responses/models.rb +0 -48
- data/lib/ruby_llm/providers/openai_responses/stream_accumulator_extension.rb +0 -39
- data/lib/ruby_llm/providers/openai_responses/streaming.rb +0 -137
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6b169a57a059b3f091ea37d5f46dea3f253acb25f3544ad5f98ddec9f81e0185
|
|
4
|
+
data.tar.gz: f5251e33d031cfb749454c344d4e88e0edd83940dced27770d0a29d2de0b71cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
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!(
|
|
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 `
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
149
|
+
chat.with_provider_options(tools: [tool])
|
|
144
150
|
```
|
|
145
151
|
|
|
146
152
|
### Image Generation
|
|
147
153
|
|
|
148
154
|
```ruby
|
|
149
|
-
chat.
|
|
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.
|
|
167
|
+
chat.with_provider_options(tools: [tool])
|
|
162
168
|
```
|
|
163
169
|
|
|
164
170
|
### Combining Tools
|
|
165
171
|
|
|
166
172
|
```ruby
|
|
167
|
-
chat.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 `
|
|
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.
|
|
276
|
+
chat.with_provider_options(tools: [{ type: 'web_search' }])
|
|
271
277
|
|
|
272
|
-
chat.
|
|
273
|
-
chat.
|
|
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
|
-
|
|
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.
|
|
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
|