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
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class OpenAIResponses
|
|
6
|
+
# Native Responses rendering/parsing, with state scoped to this provider.
|
|
7
|
+
class Protocol < OpenAI::Responses
|
|
8
|
+
def render(messages, provider_options: {}, **)
|
|
9
|
+
@response_options = RubyLLM::Support::Utils.deep_symbolize_keys(provider_options)
|
|
10
|
+
payload = super(messages, provider_options: @response_options.except(:transport), **)
|
|
11
|
+
@store_response = payload[:store] != false
|
|
12
|
+
payload
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render_payload(messages, **)
|
|
16
|
+
state = @response_options || {}
|
|
17
|
+
parent = if state.key?(:previous_response_id)
|
|
18
|
+
state[:previous_response_id]
|
|
19
|
+
elsif state[:store] != false
|
|
20
|
+
messages.rfind { |message| message.role == :assistant && message.response_id }&.response_id
|
|
21
|
+
end
|
|
22
|
+
payload = super(new_messages(messages, parent), **)
|
|
23
|
+
payload[:store] = state.fetch(:store, true)
|
|
24
|
+
payload[:previous_response_id] = parent if parent
|
|
25
|
+
payload
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def apply_compaction(payload, compaction)
|
|
29
|
+
warn_ignored_compaction_options(compaction)
|
|
30
|
+
entry = { type: 'compaction' }
|
|
31
|
+
entry[:compact_threshold] = compaction[:at] if compaction[:at]
|
|
32
|
+
payload.merge(context_management: [entry])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def apply_end_user(payload, identifier)
|
|
36
|
+
payload.merge(safety_identifier: identifier)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def supports_provider_file_references?
|
|
40
|
+
true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def parse_completion_body(data, raw:)
|
|
44
|
+
decorate_message(super, data)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_final_chunk(data)
|
|
48
|
+
decorate_message(super, data.fetch('response', {}))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Public event entry point for the standalone WebSocket client.
|
|
52
|
+
def parse_event(data)
|
|
53
|
+
build_chunk(data)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def stream_response(payload, additional_headers = {})
|
|
57
|
+
accumulator = StreamAccumulator.new
|
|
58
|
+
response = stream_events(stream_url, payload, additional_headers) do |data|
|
|
59
|
+
chunk = build_chunk(data)
|
|
60
|
+
accumulator.add(chunk)
|
|
61
|
+
yield chunk
|
|
62
|
+
end
|
|
63
|
+
accumulator.to_message(response)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def stream_events(url, payload, additional_headers = {}, &)
|
|
67
|
+
return super unless websocket?
|
|
68
|
+
|
|
69
|
+
@usage_tracker&.start
|
|
70
|
+
@provider.responses_websocket(additional_headers).stream_events(payload, &)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def sync_response(payload, additional_headers = {})
|
|
74
|
+
return super unless websocket?
|
|
75
|
+
|
|
76
|
+
stream_response(payload, additional_headers) { |chunk| @usage_tracker&.observe(chunk) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def websocket?
|
|
82
|
+
@response_options && @response_options[:transport]&.to_sym == :websocket
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def new_messages(messages, parent)
|
|
86
|
+
return messages unless parent
|
|
87
|
+
|
|
88
|
+
anchor = messages.rindex { |message| message.role == :assistant && message.response_id == parent }
|
|
89
|
+
return messages unless anchor
|
|
90
|
+
|
|
91
|
+
messages.each_with_index.filter_map { |message, index| message if message.role == :system || index > anchor }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def decorate_message(message, data)
|
|
95
|
+
message.response_id = data['id'] unless @store_response == false || data['store'] == false
|
|
96
|
+
message.built_in_tool_events = BuiltInTools.extract_events(data['output'] || [])
|
|
97
|
+
message.responses_api_body = data
|
|
98
|
+
message
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class OpenAIResponses
|
|
6
|
+
# Extends the native stream accumulator to carry built_in_tool_events and
|
|
7
|
+
# response_id from chunks through to the final assembled Message.
|
|
8
|
+
# Without this the accumulator drops everything off the Chunk it does
|
|
9
|
+
# not know about — including response_id, which breaks
|
|
10
|
+
# previous_response_id chaining on the next turn for any streamed chat.
|
|
11
|
+
class StreamAccumulator < RubyLLM::Protocol::StreamAccumulator
|
|
12
|
+
def add(chunk)
|
|
13
|
+
super
|
|
14
|
+
@response_id = chunk.response_id if chunk.respond_to?(:response_id) && chunk.response_id
|
|
15
|
+
|
|
16
|
+
@responses_api_body = chunk.responses_api_body if chunk.responses_api_body
|
|
17
|
+
events = chunk_built_in_events(chunk)
|
|
18
|
+
return if events.nil? || events.empty?
|
|
19
|
+
|
|
20
|
+
@built_in_tool_events ||= []
|
|
21
|
+
@built_in_tool_events.concat(events)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_message(response)
|
|
25
|
+
if @responses_api_body
|
|
26
|
+
# A completed stream supplies the same response object as a sync call.
|
|
27
|
+
response = Faraday::Response.new(status: response&.status || 200,
|
|
28
|
+
response_headers: response&.headers || {}, body: @responses_api_body)
|
|
29
|
+
end
|
|
30
|
+
message = super
|
|
31
|
+
if @built_in_tool_events && !@built_in_tool_events.empty? && message.respond_to?(:built_in_tool_events=)
|
|
32
|
+
message.built_in_tool_events = @built_in_tool_events
|
|
33
|
+
end
|
|
34
|
+
message.response_id = @response_id if @response_id && message.respond_to?(:response_id=)
|
|
35
|
+
message
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def chunk_built_in_events(chunk)
|
|
41
|
+
return nil unless chunk.respond_to?(:built_in_tool_events)
|
|
42
|
+
|
|
43
|
+
chunk.built_in_tool_events
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -13,7 +13,7 @@ module RubyLLM
|
|
|
13
13
|
#
|
|
14
14
|
# Integrated usage (recommended):
|
|
15
15
|
# chat = RubyLLM.chat(model: 'gpt-4o', provider: :openai_responses)
|
|
16
|
-
# chat.
|
|
16
|
+
# chat.with_provider_options(transport: :websocket)
|
|
17
17
|
# chat.ask("Hello!")
|
|
18
18
|
#
|
|
19
19
|
# Standalone usage (advanced):
|
|
@@ -34,15 +34,16 @@ module RubyLLM
|
|
|
34
34
|
# @param project_id [String, nil] OpenAI project ID
|
|
35
35
|
# @param client_class [#connect, nil] WebSocket client class (for testing)
|
|
36
36
|
# @param response_timeout [Numeric] seconds to wait for a response event
|
|
37
|
-
# rubocop:disable Metrics/ParameterLists
|
|
37
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
38
38
|
def initialize(api_key:, api_base: 'https://api.openai.com/v1', organization_id: nil, project_id: nil,
|
|
39
|
-
client_class: nil, response_timeout: 60)
|
|
39
|
+
client_class: nil, response_timeout: 60, headers: {})
|
|
40
40
|
@api_key = api_key
|
|
41
41
|
@api_base = api_base
|
|
42
42
|
@organization_id = organization_id
|
|
43
43
|
@project_id = project_id
|
|
44
44
|
@client_class = client_class
|
|
45
45
|
@response_timeout = response_timeout
|
|
46
|
+
@headers = headers
|
|
46
47
|
|
|
47
48
|
@ws = nil
|
|
48
49
|
@mutex = Mutex.new
|
|
@@ -51,7 +52,6 @@ module RubyLLM
|
|
|
51
52
|
@last_response_id = nil
|
|
52
53
|
@message_queue = nil
|
|
53
54
|
end
|
|
54
|
-
# rubocop:enable Metrics/ParameterLists
|
|
55
55
|
|
|
56
56
|
# Open the WebSocket connection. Blocks until the connection is established.
|
|
57
57
|
# @param timeout [Numeric] seconds to wait for the connection (default: 10)
|
|
@@ -96,19 +96,43 @@ module RubyLLM
|
|
|
96
96
|
# @param payload [Hash] Responses API payload (model, input, tools, etc.)
|
|
97
97
|
# @yield [RubyLLM::Chunk] each streamed chunk
|
|
98
98
|
# @return [RubyLLM::Message] the assembled final message
|
|
99
|
-
def call(payload
|
|
99
|
+
def call(payload)
|
|
100
|
+
config = RubyLLM.config.dup
|
|
101
|
+
config.openai_api_key = @api_key
|
|
102
|
+
provider = OpenAIResponses.new(config)
|
|
103
|
+
protocol = Protocol.new(provider)
|
|
104
|
+
accumulator = StreamAccumulator.new
|
|
105
|
+
stream_events(payload) do |data|
|
|
106
|
+
chunk = protocol.parse_event(data)
|
|
107
|
+
chunk.response_id = nil if payload[:store] == false
|
|
108
|
+
accumulator.add(chunk)
|
|
109
|
+
yield chunk if block_given?
|
|
110
|
+
end
|
|
111
|
+
accumulator.to_message(nil)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Shared by the standalone client and the native RubyLLM protocol.
|
|
115
|
+
def stream_events(payload)
|
|
100
116
|
ensure_connected!
|
|
101
117
|
acquire_flight!
|
|
102
|
-
|
|
118
|
+
acquired = true
|
|
103
119
|
queue = Queue.new
|
|
104
120
|
@mutex.synchronize { @message_queue = queue }
|
|
121
|
+
send_json({ type: 'response.create' }.merge(payload.except(:stream)))
|
|
122
|
+
|
|
123
|
+
loop do
|
|
124
|
+
data = next_event(queue)
|
|
125
|
+
yield data
|
|
126
|
+
next unless %w[response.completed response.incomplete].include?(data['type'])
|
|
105
127
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
128
|
+
@last_response_id = payload[:store] == false ? nil : data.dig('response', 'id')
|
|
129
|
+
return nil
|
|
130
|
+
end
|
|
109
131
|
ensure
|
|
110
|
-
|
|
111
|
-
|
|
132
|
+
if acquired
|
|
133
|
+
@mutex.synchronize { @message_queue = nil }
|
|
134
|
+
release_flight!
|
|
135
|
+
end
|
|
112
136
|
end
|
|
113
137
|
|
|
114
138
|
# Send a response.create request using raw Responses API format.
|
|
@@ -122,14 +146,14 @@ module RubyLLM
|
|
|
122
146
|
# @param extra [Hash] additional fields forwarded to the API
|
|
123
147
|
# @yield [RubyLLM::Chunk] each streamed chunk
|
|
124
148
|
# @return [RubyLLM::Message] the assembled final message
|
|
125
|
-
def create_response(model:, input:, tools: nil, previous_response_id: nil, instructions: nil, **extra, &
|
|
149
|
+
def create_response(model:, input:, tools: nil, previous_response_id: nil, instructions: nil, **extra, &) # rubocop:disable Metrics/ParameterLists
|
|
126
150
|
payload = build_standalone_payload(
|
|
127
151
|
model: model, input: input, tools: tools,
|
|
128
152
|
previous_response_id: previous_response_id,
|
|
129
153
|
instructions: instructions, **extra
|
|
130
154
|
)
|
|
131
155
|
|
|
132
|
-
call(payload, &
|
|
156
|
+
call(payload, &)
|
|
133
157
|
end
|
|
134
158
|
|
|
135
159
|
# Warm up the connection by sending a response.create with generate: false.
|
|
@@ -138,33 +162,7 @@ module RubyLLM
|
|
|
138
162
|
# @param extra [Hash] additional fields
|
|
139
163
|
# @return [void]
|
|
140
164
|
def warmup(model:, **extra)
|
|
141
|
-
|
|
142
|
-
acquire_flight!
|
|
143
|
-
|
|
144
|
-
queue = Queue.new
|
|
145
|
-
@mutex.synchronize { @message_queue = queue }
|
|
146
|
-
|
|
147
|
-
payload = { type: 'response.create', model: model, generate: false }.merge(extra)
|
|
148
|
-
|
|
149
|
-
send_json(payload)
|
|
150
|
-
|
|
151
|
-
loop do
|
|
152
|
-
data = queue.pop
|
|
153
|
-
break if data.nil?
|
|
154
|
-
|
|
155
|
-
parsed = JSON.parse(data)
|
|
156
|
-
event_type = parsed['type']
|
|
157
|
-
|
|
158
|
-
if event_type == 'error'
|
|
159
|
-
error_msg = parsed.dig('error', 'message') || 'Warmup error'
|
|
160
|
-
raise ResponseError, error_msg
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
break if event_type == 'response.completed'
|
|
164
|
-
end
|
|
165
|
-
ensure
|
|
166
|
-
@mutex.synchronize { @message_queue = nil }
|
|
167
|
-
release_flight!
|
|
165
|
+
stream_events({ model: model, generate: false }.merge(extra)) { |_event| nil }
|
|
168
166
|
end
|
|
169
167
|
|
|
170
168
|
# Disconnect the WebSocket.
|
|
@@ -226,13 +224,13 @@ module RubyLLM
|
|
|
226
224
|
}
|
|
227
225
|
headers['OpenAI-Organization'] = @organization_id if @organization_id
|
|
228
226
|
headers['OpenAI-Project'] = @project_id if @project_id
|
|
229
|
-
headers
|
|
227
|
+
headers.merge(@headers)
|
|
230
228
|
end
|
|
231
229
|
|
|
232
230
|
def build_standalone_payload(model:, input:, tools: nil, previous_response_id: nil, instructions: nil, **extra) # rubocop:disable Metrics/ParameterLists
|
|
233
|
-
prev_id = previous_response_id || @last_response_id
|
|
231
|
+
prev_id = previous_response_id || (@last_response_id unless extra[:store] == false)
|
|
234
232
|
response = { model: model, input: input }
|
|
235
|
-
response[:tools] = tools
|
|
233
|
+
response[:tools] = tools if tools&.any?
|
|
236
234
|
response[:previous_response_id] = prev_id if prev_id
|
|
237
235
|
response[:instructions] = instructions if instructions
|
|
238
236
|
|
|
@@ -247,43 +245,19 @@ module RubyLLM
|
|
|
247
245
|
@ws.send(JSON.generate(payload))
|
|
248
246
|
end
|
|
249
247
|
|
|
250
|
-
def
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
loop do
|
|
255
|
-
raw = pop_response_event(queue)
|
|
256
|
-
if raw == RESPONSE_TIMEOUT
|
|
257
|
-
raise ConnectionError, "Timed out waiting for WebSocket response after #{@response_timeout} seconds"
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
break if raw.nil?
|
|
261
|
-
|
|
262
|
-
data = JSON.parse(raw)
|
|
263
|
-
event_type = data['type']
|
|
264
|
-
|
|
265
|
-
chunk = Streaming.build_chunk(data)
|
|
266
|
-
accumulator.add(chunk)
|
|
267
|
-
block&.call(chunk)
|
|
268
|
-
|
|
269
|
-
next unless event_type == 'response.completed'
|
|
270
|
-
|
|
271
|
-
track_response_id(data)
|
|
272
|
-
built_in_events.concat(BuiltInTools.extract_events(data.dig('response', 'output') || []))
|
|
273
|
-
break
|
|
248
|
+
def next_event(queue)
|
|
249
|
+
raw = pop_response_event(queue)
|
|
250
|
+
if raw == RESPONSE_TIMEOUT
|
|
251
|
+
raise ConnectionError, "Timed out waiting for WebSocket response after #{@response_timeout} seconds"
|
|
274
252
|
end
|
|
253
|
+
raise ConnectionError, 'WebSocket closed before the response completed' if raw.nil?
|
|
275
254
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
message
|
|
255
|
+
data = JSON.parse(raw)
|
|
256
|
+
if %w[error response.failed].include?(data['type'])
|
|
257
|
+
error = data['error'] || data.dig('response', 'error') || data
|
|
258
|
+
raise RubyLLM::Error, error['message'] || 'WebSocket response failed'
|
|
280
259
|
end
|
|
281
|
-
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
def track_response_id(data)
|
|
285
|
-
resp_id = data.dig('response', 'id')
|
|
286
|
-
@mutex.synchronize { @last_response_id = resp_id } if resp_id
|
|
260
|
+
data
|
|
287
261
|
end
|
|
288
262
|
|
|
289
263
|
def ensure_connected!
|
|
@@ -6,36 +6,11 @@ module RubyLLM
|
|
|
6
6
|
# Implements the new Responses API which provides built-in tools,
|
|
7
7
|
# stateful conversations, background mode, and MCP support.
|
|
8
8
|
class OpenAIResponses
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
include OpenAIResponses::Tools
|
|
12
|
-
include OpenAIResponses::Models
|
|
13
|
-
include OpenAIResponses::Media
|
|
14
|
-
|
|
15
|
-
def api_base
|
|
16
|
-
@config.openai_api_base || 'https://api.openai.com/v1'
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# Override to support WebSocket transport via with_params(transport: :websocket).
|
|
20
|
-
# rubocop:disable Metrics/ParameterLists
|
|
21
|
-
def complete(messages, tools:, temperature:, model:, params: {}, headers: {},
|
|
22
|
-
schema: nil, thinking: nil, tool_prefs: nil, &block)
|
|
23
|
-
if params[:transport]&.to_sym == :websocket
|
|
24
|
-
ws_complete(messages, tools: tools, temperature: temperature, model: model,
|
|
25
|
-
params: params.except(:transport), schema: schema,
|
|
26
|
-
thinking: thinking, &block)
|
|
27
|
-
else
|
|
28
|
-
super
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
# rubocop:enable Metrics/ParameterLists
|
|
9
|
+
protocol :responses, OpenAIResponses::Protocol, batches: Protocols::Responses::Batches
|
|
10
|
+
protocol :files, Protocols::OpenAI::Files
|
|
32
11
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
'Authorization' => "Bearer #{@config.openai_api_key}",
|
|
36
|
-
'OpenAI-Organization' => @config.openai_organization_id,
|
|
37
|
-
'OpenAI-Project' => @config.openai_project_id
|
|
38
|
-
}.compact
|
|
12
|
+
def protocol_for(_model, **)
|
|
13
|
+
protocols.fetch(:responses)
|
|
39
14
|
end
|
|
40
15
|
|
|
41
16
|
# Retrieve a stored response by ID
|
|
@@ -185,56 +160,45 @@ module RubyLLM
|
|
|
185
160
|
response.body
|
|
186
161
|
end
|
|
187
162
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
messages,
|
|
196
|
-
tools: tools,
|
|
197
|
-
temperature: normalized_temperature,
|
|
198
|
-
model: model,
|
|
199
|
-
stream: true,
|
|
200
|
-
schema: schema,
|
|
201
|
-
thinking: thinking
|
|
202
|
-
),
|
|
203
|
-
params
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
ws_connection.connect unless ws_connection.connected?
|
|
207
|
-
ws_connection.call(payload, &block)
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def ws_connection
|
|
163
|
+
# Headers are part of the handshake, so a changed set uses a new connection.
|
|
164
|
+
def responses_websocket(additional_headers = {})
|
|
165
|
+
if @ws_connection && @ws_headers != additional_headers
|
|
166
|
+
@ws_connection.disconnect
|
|
167
|
+
@ws_connection = nil
|
|
168
|
+
end
|
|
169
|
+
@ws_headers = additional_headers.dup
|
|
211
170
|
@ws_connection ||= WebSocket.new(
|
|
212
|
-
api_key: @config.openai_api_key,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
project_id: @config.openai_project_id
|
|
171
|
+
api_key: @config.openai_api_key, api_base: api_base,
|
|
172
|
+
organization_id: @config.openai_organization_id, project_id: @config.openai_project_id,
|
|
173
|
+
headers: additional_headers
|
|
216
174
|
)
|
|
175
|
+
@ws_connection.connect unless @ws_connection.connected?
|
|
176
|
+
@ws_connection
|
|
217
177
|
end
|
|
218
178
|
|
|
219
|
-
|
|
220
|
-
|
|
179
|
+
private
|
|
180
|
+
|
|
221
181
|
def delete_request(url)
|
|
222
|
-
@connection.
|
|
223
|
-
req.headers.merge!(headers)
|
|
224
|
-
end
|
|
182
|
+
@connection.delete(url)
|
|
225
183
|
end
|
|
226
184
|
|
|
227
185
|
class << self
|
|
228
|
-
def
|
|
229
|
-
|
|
186
|
+
def configuration_options
|
|
187
|
+
%i[
|
|
188
|
+
openai_api_key
|
|
189
|
+
openai_api_base
|
|
190
|
+
openai_organization_id
|
|
191
|
+
openai_project_id
|
|
192
|
+
]
|
|
230
193
|
end
|
|
231
194
|
|
|
232
195
|
def configuration_requirements
|
|
233
196
|
%i[openai_api_key]
|
|
234
197
|
end
|
|
235
198
|
|
|
236
|
-
def
|
|
237
|
-
|
|
199
|
+
def models_dev_alias(model_id, models, provider_model = nil)
|
|
200
|
+
source = models["openai:#{model_id}"] || super
|
|
201
|
+
source && RubyLLM::Model.new(source.to_h.merge(provider: 'openai_responses'))
|
|
238
202
|
end
|
|
239
203
|
end
|
|
240
204
|
end
|
|
@@ -1,49 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'ruby_llm'
|
|
4
|
-
|
|
5
|
-
# Provider class must be loaded first to define the class
|
|
6
4
|
require_relative 'ruby_llm/providers/openai_responses/base'
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
require_relative
|
|
13
|
-
require_relative 'ruby_llm/providers/openai_responses/streaming'
|
|
14
|
-
require_relative 'ruby_llm/providers/openai_responses/chat'
|
|
15
|
-
|
|
16
|
-
# Advanced features
|
|
17
|
-
require_relative 'ruby_llm/providers/openai_responses/built_in_tools'
|
|
18
|
-
require_relative 'ruby_llm/providers/openai_responses/state'
|
|
19
|
-
require_relative 'ruby_llm/providers/openai_responses/background'
|
|
20
|
-
require_relative 'ruby_llm/providers/openai_responses/compaction'
|
|
21
|
-
require_relative 'ruby_llm/providers/openai_responses/containers'
|
|
22
|
-
require_relative 'ruby_llm/providers/openai_responses/batches'
|
|
23
|
-
require_relative 'ruby_llm/providers/openai_responses/batch'
|
|
24
|
-
require_relative 'ruby_llm/providers/openai_responses/message_extension'
|
|
25
|
-
require_relative 'ruby_llm/providers/openai_responses/stream_accumulator_extension'
|
|
26
|
-
require_relative 'ruby_llm/providers/openai_responses/chat_extension'
|
|
27
|
-
require_relative 'ruby_llm/providers/openai_responses/model_registry'
|
|
28
|
-
require_relative 'ruby_llm/providers/openai_responses/active_record_extension'
|
|
29
|
-
require_relative 'ruby_llm/providers/openai_responses/web_socket'
|
|
30
|
-
|
|
31
|
-
# Include all modules in the provider class
|
|
6
|
+
%w[
|
|
7
|
+
built_in_tools state background compaction containers message_extension
|
|
8
|
+
stream_accumulator chat_extension model_registry active_record_extension
|
|
9
|
+
protocol web_socket batches batch
|
|
10
|
+
].each { |name| require_relative "ruby_llm/providers/openai_responses/#{name}" }
|
|
32
11
|
require_relative 'ruby_llm/providers/openai_responses'
|
|
33
12
|
|
|
34
|
-
# Register the provider
|
|
35
13
|
RubyLLM::Provider.register :openai_responses, RubyLLM::Providers::OpenAIResponses
|
|
36
14
|
|
|
37
|
-
# Register models for this provider
|
|
38
|
-
RubyLLM::Providers::OpenAIResponses::ModelRegistry.register_all!
|
|
39
|
-
|
|
40
|
-
# Extend RubyLLM module with ResponsesAPI namespace
|
|
41
15
|
module RubyLLM
|
|
42
|
-
#
|
|
16
|
+
# Responses-specific helpers. RubyLLM's native batch factory is unchanged.
|
|
43
17
|
module ResponsesAPI
|
|
44
|
-
VERSION = '0.
|
|
18
|
+
VERSION = '1.0.0'
|
|
45
19
|
|
|
46
|
-
# Shorthand access to built-in tool helpers
|
|
47
20
|
BuiltInTools = Providers::OpenAIResponses::BuiltInTools
|
|
48
21
|
State = Providers::OpenAIResponses::State
|
|
49
22
|
Background = Providers::OpenAIResponses::Background
|
|
@@ -52,19 +25,13 @@ module RubyLLM
|
|
|
52
25
|
Batches = Providers::OpenAIResponses::Batches
|
|
53
26
|
Batch = Providers::OpenAIResponses::Batch
|
|
54
27
|
WebSocket = Providers::OpenAIResponses::WebSocket
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Create a new Batch for bulk request processing
|
|
58
|
-
def self.batch(...)
|
|
59
|
-
Providers::OpenAIResponses::Batch.new(...)
|
|
60
|
-
end
|
|
61
28
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
provider_class = Provider.providers[slug]
|
|
66
|
-
raise Error.new(nil, "Unknown provider: #{slug}") unless provider_class
|
|
29
|
+
def self.batch(...)
|
|
30
|
+
Batch.new(...)
|
|
31
|
+
end
|
|
67
32
|
|
|
68
|
-
|
|
33
|
+
def self.batches(provider: :openai_responses, **)
|
|
34
|
+
RubyLLM::Provider.resolve!(provider).new(RubyLLM.config).list_batches(**)
|
|
35
|
+
end
|
|
69
36
|
end
|
|
70
37
|
end
|
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
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chris Hasinski
|
|
@@ -13,16 +13,16 @@ dependencies:
|
|
|
13
13
|
name: ruby_llm
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
18
|
+
version: 2.1.1
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
|
-
- - "
|
|
23
|
+
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
25
|
+
version: 2.1.1
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: activerecord
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -83,16 +83,16 @@ dependencies:
|
|
|
83
83
|
name: sqlite3
|
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
|
85
85
|
requirements:
|
|
86
|
-
- - "
|
|
86
|
+
- - ">="
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
|
-
version:
|
|
88
|
+
version: 2.9.5
|
|
89
89
|
type: :development
|
|
90
90
|
prerelease: false
|
|
91
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
92
|
requirements:
|
|
93
|
-
- - "
|
|
93
|
+
- - ">="
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
|
-
version:
|
|
95
|
+
version: 2.9.5
|
|
96
96
|
- !ruby/object:Gem::Dependency
|
|
97
97
|
name: vcr
|
|
98
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -147,6 +147,7 @@ extra_rdoc_files: []
|
|
|
147
147
|
files:
|
|
148
148
|
- CHANGELOG.md
|
|
149
149
|
- LICENSE.txt
|
|
150
|
+
- MIGRATION.md
|
|
150
151
|
- README.md
|
|
151
152
|
- lib/ruby_llm-responses_api.rb
|
|
152
153
|
- lib/ruby_llm/providers/openai_responses.rb
|
|
@@ -156,19 +157,14 @@ files:
|
|
|
156
157
|
- lib/ruby_llm/providers/openai_responses/batch.rb
|
|
157
158
|
- lib/ruby_llm/providers/openai_responses/batches.rb
|
|
158
159
|
- lib/ruby_llm/providers/openai_responses/built_in_tools.rb
|
|
159
|
-
- lib/ruby_llm/providers/openai_responses/capabilities.rb
|
|
160
|
-
- lib/ruby_llm/providers/openai_responses/chat.rb
|
|
161
160
|
- lib/ruby_llm/providers/openai_responses/chat_extension.rb
|
|
162
161
|
- lib/ruby_llm/providers/openai_responses/compaction.rb
|
|
163
162
|
- lib/ruby_llm/providers/openai_responses/containers.rb
|
|
164
|
-
- lib/ruby_llm/providers/openai_responses/media.rb
|
|
165
163
|
- lib/ruby_llm/providers/openai_responses/message_extension.rb
|
|
166
164
|
- lib/ruby_llm/providers/openai_responses/model_registry.rb
|
|
167
|
-
- lib/ruby_llm/providers/openai_responses/
|
|
165
|
+
- lib/ruby_llm/providers/openai_responses/protocol.rb
|
|
168
166
|
- lib/ruby_llm/providers/openai_responses/state.rb
|
|
169
|
-
- lib/ruby_llm/providers/openai_responses/
|
|
170
|
-
- lib/ruby_llm/providers/openai_responses/streaming.rb
|
|
171
|
-
- lib/ruby_llm/providers/openai_responses/tools.rb
|
|
167
|
+
- lib/ruby_llm/providers/openai_responses/stream_accumulator.rb
|
|
172
168
|
- lib/ruby_llm/providers/openai_responses/web_socket.rb
|
|
173
169
|
- lib/rubyllm_responses_api.rb
|
|
174
170
|
homepage: https://github.com/khasinski/ruby_llm-responses_api
|
|
@@ -186,14 +182,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
186
182
|
requirements:
|
|
187
183
|
- - ">="
|
|
188
184
|
- !ruby/object:Gem::Version
|
|
189
|
-
version:
|
|
185
|
+
version: '4.0'
|
|
186
|
+
- - "<"
|
|
187
|
+
- !ruby/object:Gem::Version
|
|
188
|
+
version: '5.0'
|
|
190
189
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
191
190
|
requirements:
|
|
192
191
|
- - ">="
|
|
193
192
|
- !ruby/object:Gem::Version
|
|
194
193
|
version: '0'
|
|
195
194
|
requirements: []
|
|
196
|
-
rubygems_version:
|
|
195
|
+
rubygems_version: 4.0.20
|
|
197
196
|
specification_version: 4
|
|
198
197
|
summary: OpenAI Responses API provider for RubyLLM
|
|
199
198
|
test_files: []
|