ruby_llm-responses_api 0.4.0 → 0.4.1
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 +11 -0
- data/README.md +18 -37
- data/lib/ruby_llm/providers/openai_responses/web_socket.rb +40 -36
- data/lib/ruby_llm/providers/openai_responses.rb +39 -0
- data/lib/rubyllm_responses_api.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c2d9ce65eebe6420f01878669d81f90f999b738158b17eaa558dd6c88226c2c2
|
|
4
|
+
data.tar.gz: c432ef2dfcebb290debbbc5ac5e72038081f54fc054065da1bee09465ba99ba0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 39bbbb38a8b7183ff501d092eab938f0ab6572129ca3cd518057daa04b21117ea38eb8c19ab1a6755036b41f683f3124a1c0209ee91c5f547fe12590b673bbf3
|
|
7
|
+
data.tar.gz: 74346a9093b98f079b02deffc3d9f8cbe9b8bf681d33a843d4bd130d099976f78d66a61a6c2b5032d31a84190e25b7509fe4e83f39fa278be2f74f5980568544
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ 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
|
+
## [0.4.1] - 2026-02-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `chat.with_params(transport: :websocket)` integration with standard `chat.ask` interface
|
|
13
|
+
- `WebSocket#call` for accepting pre-built payloads from the provider
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- WebSocket responses now preserve token counts from `StreamAccumulator`
|
|
18
|
+
|
|
8
19
|
## [0.4.0] - 2026-02-24
|
|
9
20
|
|
|
10
21
|
### Added
|
data/README.md
CHANGED
|
@@ -269,59 +269,40 @@ Requires the `websocket-client-simple` gem:
|
|
|
269
269
|
gem 'websocket-client-simple'
|
|
270
270
|
```
|
|
271
271
|
|
|
272
|
-
###
|
|
272
|
+
### Usage
|
|
273
273
|
|
|
274
|
-
|
|
275
|
-
ws = RubyLLM::ResponsesAPI::WebSocket.new(api_key: ENV['OPENAI_API_KEY'])
|
|
276
|
-
ws.connect
|
|
274
|
+
Just add `transport: :websocket` to your params -- the standard `chat.ask` API works as-is:
|
|
277
275
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
input: [{ type: 'message', role: 'user', content: 'Hello!' }]
|
|
282
|
-
) do |chunk|
|
|
283
|
-
print chunk.content if chunk.content
|
|
284
|
-
end
|
|
276
|
+
```ruby
|
|
277
|
+
chat = RubyLLM.chat(model: 'gpt-4o', provider: :openai_responses)
|
|
278
|
+
chat.with_params(transport: :websocket)
|
|
285
279
|
|
|
286
|
-
|
|
280
|
+
chat.ask("Hello!")
|
|
281
|
+
chat.ask("What's 2+2?") # reuses the same WebSocket connection
|
|
287
282
|
```
|
|
288
283
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
`previous_response_id` is tracked automatically across turns:
|
|
284
|
+
Streaming works the same way:
|
|
292
285
|
|
|
293
286
|
```ruby
|
|
294
|
-
|
|
295
|
-
{ type: 'message', role: 'user', content: 'My name is Alice.' }
|
|
296
|
-
])
|
|
297
|
-
|
|
298
|
-
ws.create_response(model: 'gpt-4o', input: [
|
|
299
|
-
{ type: 'message', role: 'user', content: "What's my name?" }
|
|
300
|
-
])
|
|
301
|
-
# => "Alice" (auto-chained via previous_response_id)
|
|
287
|
+
chat.ask("Tell me a story") { |chunk| print chunk.content }
|
|
302
288
|
```
|
|
303
289
|
|
|
304
|
-
###
|
|
290
|
+
### Direct WebSocket access
|
|
291
|
+
|
|
292
|
+
For advanced use cases (raw Responses API format, warmup, explicit connection management):
|
|
305
293
|
|
|
306
294
|
```ruby
|
|
295
|
+
ws = RubyLLM::ResponsesAPI::WebSocket.new(api_key: ENV['OPENAI_API_KEY'])
|
|
296
|
+
ws.connect
|
|
297
|
+
|
|
307
298
|
ws.create_response(
|
|
308
299
|
model: 'gpt-4o',
|
|
309
|
-
input: [{ type: 'message', role: 'user', content: '
|
|
310
|
-
|
|
311
|
-
)
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
### Warmup
|
|
315
|
-
|
|
316
|
-
Pre-cache model weights without generating output:
|
|
300
|
+
input: [{ type: 'message', role: 'user', content: 'Hello!' }]
|
|
301
|
+
) { |chunk| print chunk.content }
|
|
317
302
|
|
|
318
|
-
|
|
303
|
+
# Pre-cache model weights
|
|
319
304
|
ws.warmup(model: 'gpt-4o')
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
### Cleanup
|
|
323
305
|
|
|
324
|
-
```ruby
|
|
325
306
|
ws.disconnect
|
|
326
307
|
```
|
|
327
308
|
|
|
@@ -11,14 +11,15 @@ module RubyLLM
|
|
|
11
11
|
#
|
|
12
12
|
# Requires the `websocket-client-simple` gem (soft dependency).
|
|
13
13
|
#
|
|
14
|
-
#
|
|
14
|
+
# Integrated usage (recommended):
|
|
15
|
+
# chat = RubyLLM.chat(model: 'gpt-4o', provider: :openai_responses)
|
|
16
|
+
# chat.with_params(transport: :websocket)
|
|
17
|
+
# chat.ask("Hello!")
|
|
18
|
+
#
|
|
19
|
+
# Standalone usage (advanced):
|
|
15
20
|
# ws = RubyLLM::ResponsesAPI::WebSocket.new(api_key: ENV['OPENAI_API_KEY'])
|
|
16
21
|
# ws.connect
|
|
17
|
-
#
|
|
18
|
-
# ws.create_response(model: 'gpt-4o', input: [{ type: 'message', role: 'user', content: 'Hi' }]) do |chunk|
|
|
19
|
-
# print chunk.content if chunk.content
|
|
20
|
-
# end
|
|
21
|
-
#
|
|
22
|
+
# ws.create_response(model: 'gpt-4o', input: [...]) { |chunk| ... }
|
|
22
23
|
# ws.disconnect
|
|
23
24
|
class WebSocket
|
|
24
25
|
WEBSOCKET_PATH = '/v1/responses'
|
|
@@ -73,7 +74,6 @@ module RubyLLM
|
|
|
73
74
|
end
|
|
74
75
|
end
|
|
75
76
|
|
|
76
|
-
# Route all messages to the current queue (swapped per request)
|
|
77
77
|
@ws.on(:message) do |msg|
|
|
78
78
|
q = @mutex.synchronize { @message_queue }
|
|
79
79
|
q&.push(msg.data)
|
|
@@ -89,35 +89,47 @@ module RubyLLM
|
|
|
89
89
|
self
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
# Send a
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
# @param
|
|
97
|
-
# @param instructions [String, nil] system/developer instructions
|
|
98
|
-
# @param extra [Hash] additional top-level fields forwarded to the API
|
|
92
|
+
# Send a pre-built payload over WebSocket, streaming chunks via block.
|
|
93
|
+
# This is the integration point for Provider#complete -- it accepts the
|
|
94
|
+
# same payload hash that render_payload returns.
|
|
95
|
+
#
|
|
96
|
+
# @param payload [Hash] Responses API payload (model, input, tools, etc.)
|
|
99
97
|
# @yield [RubyLLM::Chunk] each streamed chunk
|
|
100
98
|
# @return [RubyLLM::Message] the assembled final message
|
|
101
|
-
|
|
102
|
-
# @raise [ConnectionError] if not connected
|
|
103
|
-
def create_response(model:, input:, tools: nil, previous_response_id: nil, instructions: nil, **extra, &block)
|
|
99
|
+
def call(payload, &block)
|
|
104
100
|
ensure_connected!
|
|
105
101
|
acquire_flight!
|
|
106
102
|
|
|
107
103
|
queue = Queue.new
|
|
108
104
|
@mutex.synchronize { @message_queue = queue }
|
|
109
105
|
|
|
110
|
-
|
|
106
|
+
envelope = { type: 'response.create', response: payload.except(:stream) }
|
|
107
|
+
send_json(envelope)
|
|
108
|
+
accumulate_response(queue, &block)
|
|
109
|
+
ensure
|
|
110
|
+
@mutex.synchronize { @message_queue = nil }
|
|
111
|
+
release_flight!
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Send a response.create request using raw Responses API format.
|
|
115
|
+
# Useful for standalone usage outside the RubyLLM chat interface.
|
|
116
|
+
#
|
|
117
|
+
# @param model [String] model ID
|
|
118
|
+
# @param input [Array<Hash>] input items in Responses API format
|
|
119
|
+
# @param tools [Array<Hash>, nil] tool definitions
|
|
120
|
+
# @param previous_response_id [String, nil] chain to a prior response
|
|
121
|
+
# @param instructions [String, nil] system/developer instructions
|
|
122
|
+
# @param extra [Hash] additional fields forwarded to the API
|
|
123
|
+
# @yield [RubyLLM::Chunk] each streamed chunk
|
|
124
|
+
# @return [RubyLLM::Message] the assembled final message
|
|
125
|
+
def create_response(model:, input:, tools: nil, previous_response_id: nil, instructions: nil, **extra, &block)
|
|
126
|
+
payload = build_standalone_payload(
|
|
111
127
|
model: model, input: input, tools: tools,
|
|
112
128
|
previous_response_id: previous_response_id,
|
|
113
129
|
instructions: instructions, **extra
|
|
114
130
|
)
|
|
115
131
|
|
|
116
|
-
|
|
117
|
-
accumulate_response(queue, &block)
|
|
118
|
-
ensure
|
|
119
|
-
@mutex.synchronize { @message_queue = nil }
|
|
120
|
-
release_flight!
|
|
132
|
+
call(payload, &block)
|
|
121
133
|
end
|
|
122
134
|
|
|
123
135
|
# Warm up the connection by sending a response.create with generate: false.
|
|
@@ -209,7 +221,7 @@ module RubyLLM
|
|
|
209
221
|
headers
|
|
210
222
|
end
|
|
211
223
|
|
|
212
|
-
def
|
|
224
|
+
def build_standalone_payload(model:, input:, tools: nil, previous_response_id: nil, instructions: nil, **extra)
|
|
213
225
|
prev_id = previous_response_id || @last_response_id
|
|
214
226
|
response = { model: model, input: input }
|
|
215
227
|
response[:tools] = tools.map { |t| Tools.tool_for(t) } if tools&.any?
|
|
@@ -220,7 +232,7 @@ module RubyLLM
|
|
|
220
232
|
Compaction.apply_compaction(response, extra)
|
|
221
233
|
|
|
222
234
|
forwarded = extra.reject { |k, _| KNOWN_PARAMS.include?(k) }
|
|
223
|
-
|
|
235
|
+
response.merge(forwarded)
|
|
224
236
|
end
|
|
225
237
|
|
|
226
238
|
def send_json(payload)
|
|
@@ -247,7 +259,9 @@ module RubyLLM
|
|
|
247
259
|
end
|
|
248
260
|
end
|
|
249
261
|
|
|
250
|
-
|
|
262
|
+
message = accumulator.to_message(nil)
|
|
263
|
+
message.response_id = @last_response_id
|
|
264
|
+
message
|
|
251
265
|
end
|
|
252
266
|
|
|
253
267
|
def track_response_id(data)
|
|
@@ -255,16 +269,6 @@ module RubyLLM
|
|
|
255
269
|
@mutex.synchronize { @last_response_id = resp_id } if resp_id
|
|
256
270
|
end
|
|
257
271
|
|
|
258
|
-
def build_final_message(accumulator)
|
|
259
|
-
Message.new(
|
|
260
|
-
role: :assistant,
|
|
261
|
-
content: accumulator.content,
|
|
262
|
-
tool_calls: accumulator.tool_calls.empty? ? nil : accumulator.tool_calls,
|
|
263
|
-
model_id: accumulator.model_id,
|
|
264
|
-
response_id: @last_response_id
|
|
265
|
-
)
|
|
266
|
-
end
|
|
267
|
-
|
|
268
272
|
def ensure_connected!
|
|
269
273
|
raise ConnectionError, 'WebSocket is not connected. Call #connect first.' unless connected?
|
|
270
274
|
end
|
|
@@ -16,6 +16,16 @@ module RubyLLM
|
|
|
16
16
|
@config.openai_api_base || 'https://api.openai.com/v1'
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# Override to support WebSocket transport via with_params(transport: :websocket)
|
|
20
|
+
def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, thinking: nil, &block) # rubocop:disable Metrics/ParameterLists
|
|
21
|
+
if params[:transport]&.to_sym == :websocket
|
|
22
|
+
ws_complete(messages, tools: tools, temperature: temperature, model: model,
|
|
23
|
+
params: params.except(:transport), schema: schema, thinking: thinking, &block)
|
|
24
|
+
else
|
|
25
|
+
super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
19
29
|
def headers
|
|
20
30
|
{
|
|
21
31
|
'Authorization' => "Bearer #{@config.openai_api_key}",
|
|
@@ -137,6 +147,35 @@ module RubyLLM
|
|
|
137
147
|
|
|
138
148
|
private
|
|
139
149
|
|
|
150
|
+
def ws_complete(messages, tools:, temperature:, model:, params:, schema:, thinking:, &block)
|
|
151
|
+
normalized_temperature = maybe_normalize_temperature(temperature, model)
|
|
152
|
+
|
|
153
|
+
payload = Utils.deep_merge(
|
|
154
|
+
render_payload(
|
|
155
|
+
messages,
|
|
156
|
+
tools: tools,
|
|
157
|
+
temperature: normalized_temperature,
|
|
158
|
+
model: model,
|
|
159
|
+
stream: true,
|
|
160
|
+
schema: schema,
|
|
161
|
+
thinking: thinking
|
|
162
|
+
),
|
|
163
|
+
params
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
ws_connection.connect unless ws_connection.connected?
|
|
167
|
+
ws_connection.call(payload, &block)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def ws_connection
|
|
171
|
+
@ws_connection ||= WebSocket.new(
|
|
172
|
+
api_key: @config.openai_api_key,
|
|
173
|
+
api_base: api_base,
|
|
174
|
+
organization_id: @config.openai_organization_id,
|
|
175
|
+
project_id: @config.openai_project_id
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
140
179
|
# DELETE request via the underlying Faraday connection
|
|
141
180
|
# RubyLLM::Connection only exposes get/post, so we use Faraday directly
|
|
142
181
|
def delete_request(url)
|
|
@@ -37,7 +37,7 @@ RubyLLM::Providers::OpenAIResponses::ModelRegistry.register_all!
|
|
|
37
37
|
module RubyLLM
|
|
38
38
|
# ResponsesAPI namespace for direct access to helpers and version
|
|
39
39
|
module ResponsesAPI
|
|
40
|
-
VERSION = '0.4.
|
|
40
|
+
VERSION = '0.4.1'
|
|
41
41
|
|
|
42
42
|
# Shorthand access to built-in tool helpers
|
|
43
43
|
BuiltInTools = Providers::OpenAIResponses::BuiltInTools
|