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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ca7cab6681d016096c3c578e5cb0c74f21af60ec03cc4fd8667263a95cd97ce
4
- data.tar.gz: ed3d4931a835334aba4c351da61f4293464cfbeb3a3fb8399468b0a3665c962c
3
+ metadata.gz: c2d9ce65eebe6420f01878669d81f90f999b738158b17eaa558dd6c88226c2c2
4
+ data.tar.gz: c432ef2dfcebb290debbbc5ac5e72038081f54fc054065da1bee09465ba99ba0
5
5
  SHA512:
6
- metadata.gz: 4ab75bc29fe723177cd82c988b89f298e367e363d9224998bf3cde0372eb94f153804b6ffc3f8ac75032a137c1ec7fe1d065ca7d7d8452dadabc0d27d24abfa9
7
- data.tar.gz: e4b6f9837af18c683392a3436942e4aed6e03d245ab1ae0070b95c20111ec0aae01c35f44fd929808fe75c926257b2e2a0218b527f9f12744e8003d7decc6df4
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
- ### Basic usage
272
+ ### Usage
273
273
 
274
- ```ruby
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
- # Stream a response
279
- message = ws.create_response(
280
- model: 'gpt-4o',
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
- puts "\n#{message.content}"
280
+ chat.ask("Hello!")
281
+ chat.ask("What's 2+2?") # reuses the same WebSocket connection
287
282
  ```
288
283
 
289
- ### Multi-turn conversations
290
-
291
- `previous_response_id` is tracked automatically across turns:
284
+ Streaming works the same way:
292
285
 
293
286
  ```ruby
294
- ws.create_response(model: 'gpt-4o', input: [
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
- ### With tools
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: 'Search for Ruby 3.4 release notes' }],
310
- tools: [{ type: 'web_search_preview' }]
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
- ```ruby
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
- # Usage:
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 response.create request and stream chunks via block.
93
- # @param model [String] model ID
94
- # @param input [Array<Hash>] input items in Responses API format
95
- # @param tools [Array<Hash>, nil] tool definitions
96
- # @param previous_response_id [String, nil] chain to a prior response
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
- # @raise [ConcurrencyError] if another response is already in flight
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
- payload = build_payload(
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
- send_json(payload)
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 build_payload(model:, input:, tools: nil, previous_response_id: nil, instructions: nil, **extra)
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
- { type: 'response.create', response: response.merge(forwarded) }
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
- build_final_message(accumulator)
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.0'
40
+ VERSION = '0.4.1'
41
41
 
42
42
  # Shorthand access to built-in tool helpers
43
43
  BuiltInTools = Providers::OpenAIResponses::BuiltInTools
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm-responses_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Hasinski