@@ -11,14 +11,15 @@ class OpenAIResponses
1111 #
1212 # Requires the `websocket-client-simple` gem (soft dependency).
1313 #
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):
1520 # ws = RubyLLM::ResponsesAPI::WebSocket.new(api_key: ENV['OPENAI_API_KEY'])
1621 # 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| ... }
2223 # ws.disconnect
2324 class WebSocket
2425 WEBSOCKET_PATH = '/v1/responses'
@@ -73,7 +74,6 @@ def connect(timeout: 10)
7374 end
7475 end
7576
76- # Route all messages to the current queue (swapped per request)
7777 @ws . on ( :message ) do |msg |
7878 q = @mutex . synchronize { @message_queue }
7979 q &.push ( msg . data )
@@ -89,35 +89,47 @@ def connect(timeout: 10)
8989 self
9090 end
9191
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.)
9997 # @yield [RubyLLM::Chunk] each streamed chunk
10098 # @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 )
104100 ensure_connected!
105101 acquire_flight!
106102
107103 queue = Queue . new
108104 @mutex . synchronize { @message_queue = queue }
109105
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 (
111127 model : model , input : input , tools : tools ,
112128 previous_response_id : previous_response_id ,
113129 instructions : instructions , **extra
114130 )
115131
116- send_json ( payload )
117- accumulate_response ( queue , &block )
118- ensure
119- @mutex . synchronize { @message_queue = nil }
120- release_flight!
132+ call ( payload , &block )
121133 end
122134
123135 # Warm up the connection by sending a response.create with generate: false.
@@ -209,7 +221,7 @@ def build_headers
209221 headers
210222 end
211223
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 )
213225 prev_id = previous_response_id || @last_response_id
214226 response = { model : model , input : input }
215227 response [ :tools ] = tools . map { |t | Tools . tool_for ( t ) } if tools &.any?
@@ -220,7 +232,7 @@ def build_payload(model:, input:, tools: nil, previous_response_id: nil, instruc
220232 Compaction . apply_compaction ( response , extra )
221233
222234 forwarded = extra . reject { |k , _ | KNOWN_PARAMS . include? ( k ) }
223- { type : ' response.create' , response : response . merge ( forwarded ) }
235+ response . merge ( forwarded )
224236 end
225237
226238 def send_json ( payload )
@@ -247,24 +259,16 @@ def accumulate_response(queue, &block)
247259 end
248260 end
249261
250- build_final_message ( accumulator )
262+ message = accumulator . to_message ( nil )
263+ message . response_id = @last_response_id
264+ message
251265 end
252266
253267 def track_response_id ( data )
254268 resp_id = data . dig ( 'response' , 'id' )
255269 @mutex . synchronize { @last_response_id = resp_id } if resp_id
256270 end
257271
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-
268272 def ensure_connected!
269273 raise ConnectionError , 'WebSocket is not connected. Call #connect first.' unless connected?
270274 end
0 commit comments