ruby_llm-responses_api 0.6.1 → 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 +25 -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 +23 -80
- data/lib/rubyllm_responses_api.rb +13 -46
- metadata +17 -18
- data/lib/ruby_llm/providers/openai_responses/capabilities.rb +0 -267
- 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 -140
- data/lib/ruby_llm/providers/openai_responses/tools.rb +0 -222
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Providers
|
|
5
|
-
class OpenAIResponses
|
|
6
|
-
# Extends RubyLLM::StreamAccumulator to carry built_in_tool_events from
|
|
7
|
-
# chunks through to the final assembled Message. Without this the
|
|
8
|
-
# accumulator drops everything off the Chunk it does not know about.
|
|
9
|
-
module StreamAccumulatorExtension
|
|
10
|
-
def add(chunk)
|
|
11
|
-
super
|
|
12
|
-
events = chunk_built_in_events(chunk)
|
|
13
|
-
return if events.nil? || events.empty?
|
|
14
|
-
|
|
15
|
-
@built_in_tool_events ||= []
|
|
16
|
-
@built_in_tool_events.concat(events)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def to_message(response)
|
|
20
|
-
message = super
|
|
21
|
-
if @built_in_tool_events && !@built_in_tool_events.empty? && message.respond_to?(:built_in_tool_events=)
|
|
22
|
-
message.built_in_tool_events = @built_in_tool_events
|
|
23
|
-
end
|
|
24
|
-
message
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
def chunk_built_in_events(chunk)
|
|
30
|
-
return nil unless chunk.respond_to?(:built_in_tool_events)
|
|
31
|
-
|
|
32
|
-
chunk.built_in_tool_events
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
RubyLLM::StreamAccumulator.prepend(RubyLLM::Providers::OpenAIResponses::StreamAccumulatorExtension)
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Providers
|
|
5
|
-
class OpenAIResponses
|
|
6
|
-
# Streaming methods for the OpenAI Responses API.
|
|
7
|
-
# Handles SSE events with typed event format.
|
|
8
|
-
module Streaming
|
|
9
|
-
module_function
|
|
10
|
-
|
|
11
|
-
def stream_url
|
|
12
|
-
'responses'
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def build_chunk(data) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
16
|
-
event_type = data['type']
|
|
17
|
-
|
|
18
|
-
case event_type
|
|
19
|
-
when 'response.output_text.delta'
|
|
20
|
-
# Text content delta
|
|
21
|
-
Chunk.new(
|
|
22
|
-
role: :assistant,
|
|
23
|
-
content: data['delta'],
|
|
24
|
-
model_id: data.dig('response', 'model')
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
when 'response.function_call_arguments.delta'
|
|
28
|
-
# Function call arguments streaming
|
|
29
|
-
Chunk.new(
|
|
30
|
-
role: :assistant,
|
|
31
|
-
content: nil,
|
|
32
|
-
tool_calls: build_streaming_tool_call(data),
|
|
33
|
-
model_id: data.dig('response', 'model')
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
when 'response.completed'
|
|
37
|
-
# Final response with usage stats and any server-side built-in
|
|
38
|
-
# tool activity (web_search_call, code_interpreter_call, etc.) that
|
|
39
|
-
# the model executed. StreamAccumulatorExtension forwards
|
|
40
|
-
# built_in_tool_events onto the assembled Message.
|
|
41
|
-
response_data = data['response'] || {}
|
|
42
|
-
usage = response_data['usage'] || {}
|
|
43
|
-
cached_tokens = usage.dig('input_tokens_details', 'cached_tokens')
|
|
44
|
-
built_in_events = BuiltInTools.extract_events(response_data['output'] || [])
|
|
45
|
-
|
|
46
|
-
Chunk.new(
|
|
47
|
-
role: :assistant,
|
|
48
|
-
content: nil,
|
|
49
|
-
input_tokens: usage['input_tokens'],
|
|
50
|
-
output_tokens: usage['output_tokens'],
|
|
51
|
-
cached_tokens: cached_tokens,
|
|
52
|
-
cache_creation_tokens: 0,
|
|
53
|
-
model_id: response_data['model'],
|
|
54
|
-
response_id: response_data['id'],
|
|
55
|
-
built_in_tool_events: built_in_events.empty? ? nil : built_in_events
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
when 'response.output_item.added'
|
|
59
|
-
# New output item started (function call, message, etc.)
|
|
60
|
-
item = data['item'] || {}
|
|
61
|
-
if item['type'] == 'function_call'
|
|
62
|
-
stream_key = item['id'] || item['call_id']
|
|
63
|
-
Chunk.new(
|
|
64
|
-
role: :assistant,
|
|
65
|
-
content: nil,
|
|
66
|
-
tool_calls: {
|
|
67
|
-
stream_key => ToolCall.new(
|
|
68
|
-
id: item['call_id'],
|
|
69
|
-
name: item['name'],
|
|
70
|
-
arguments: ''
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
)
|
|
74
|
-
else
|
|
75
|
-
# Other item types - return empty chunk
|
|
76
|
-
Chunk.new(role: :assistant, content: nil)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
when 'response.content_part.added', 'response.content_part.done',
|
|
80
|
-
'response.output_item.done', 'response.output_text.done',
|
|
81
|
-
'response.function_call_arguments.done', 'response.created',
|
|
82
|
-
'response.in_progress'
|
|
83
|
-
# Status events - return empty chunk
|
|
84
|
-
Chunk.new(role: :assistant, content: nil)
|
|
85
|
-
|
|
86
|
-
when 'error'
|
|
87
|
-
# Error event
|
|
88
|
-
error_data = data['error'] || {}
|
|
89
|
-
raise RubyLLM::Error.new(nil, error_data['message'] || 'Unknown streaming error')
|
|
90
|
-
|
|
91
|
-
else
|
|
92
|
-
# Unknown event type - return empty chunk
|
|
93
|
-
Chunk.new(role: :assistant, content: nil)
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def build_streaming_tool_call(data)
|
|
98
|
-
call_id = data['call_id'] || data['item_id']
|
|
99
|
-
return nil unless call_id
|
|
100
|
-
|
|
101
|
-
stream_key = data['item_id']
|
|
102
|
-
stream_key ||= data['call_id'] if data['call_id']&.start_with?('call_')
|
|
103
|
-
|
|
104
|
-
# Argument delta events usually carry an item_id, while the final
|
|
105
|
-
# function call id lives on output_item.added. When only an unmapped
|
|
106
|
-
# id is available, use nil so RubyLLM 1.16 appends to the latest call.
|
|
107
|
-
{
|
|
108
|
-
stream_key => ToolCall.new(
|
|
109
|
-
id: data['name'] ? call_id : nil,
|
|
110
|
-
name: data['name'],
|
|
111
|
-
arguments: data['delta'] || ''
|
|
112
|
-
)
|
|
113
|
-
}
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def parse_streaming_error(data)
|
|
117
|
-
error_data = JSON.parse(data)
|
|
118
|
-
return unless error_data['error'] || error_data['type'] == 'error'
|
|
119
|
-
|
|
120
|
-
error = error_data['error'] || error_data
|
|
121
|
-
error_type = error['type'] || error['code']
|
|
122
|
-
error_message = error['message']
|
|
123
|
-
|
|
124
|
-
case error_type
|
|
125
|
-
when 'server_error', 'internal_error'
|
|
126
|
-
[500, error_message]
|
|
127
|
-
when 'rate_limit_exceeded', 'insufficient_quota'
|
|
128
|
-
[429, error_message]
|
|
129
|
-
when 'invalid_request_error', 'invalid_api_key'
|
|
130
|
-
[400, error_message]
|
|
131
|
-
else
|
|
132
|
-
[400, error_message]
|
|
133
|
-
end
|
|
134
|
-
rescue JSON::ParserError
|
|
135
|
-
[500, data]
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Providers
|
|
5
|
-
class OpenAIResponses
|
|
6
|
-
# Tools/function calling methods for the OpenAI Responses API.
|
|
7
|
-
# Handles both custom function tools and built-in tools.
|
|
8
|
-
module Tools
|
|
9
|
-
module_function
|
|
10
|
-
|
|
11
|
-
EMPTY_PARAMETERS_SCHEMA = {
|
|
12
|
-
'type' => 'object',
|
|
13
|
-
'properties' => {},
|
|
14
|
-
'required' => [],
|
|
15
|
-
'additionalProperties' => false
|
|
16
|
-
}.freeze
|
|
17
|
-
|
|
18
|
-
# Built-in tool type constants
|
|
19
|
-
BUILT_IN_TOOLS = {
|
|
20
|
-
web_search: { type: 'web_search' },
|
|
21
|
-
web_search_preview: { type: 'web_search_preview' },
|
|
22
|
-
file_search: ->(vector_store_ids) { { type: 'file_search', vector_store_ids: vector_store_ids } },
|
|
23
|
-
code_interpreter: { type: 'code_interpreter', container: { type: 'auto' } },
|
|
24
|
-
image_generation: { type: 'image_generation' },
|
|
25
|
-
computer_use: ->(opts) { { type: 'computer_use_preview', **opts } },
|
|
26
|
-
shell: { type: 'shell', environment: { type: 'container_auto' } },
|
|
27
|
-
apply_patch: { type: 'apply_patch' }
|
|
28
|
-
}.freeze
|
|
29
|
-
|
|
30
|
-
def tool_for(tool)
|
|
31
|
-
# Check if it's a built-in tool specification
|
|
32
|
-
return tool if tool.is_a?(Hash) && tool[:type]
|
|
33
|
-
|
|
34
|
-
# Handle symbol references to built-in tools
|
|
35
|
-
if tool.is_a?(Symbol) && BUILT_IN_TOOLS.key?(tool)
|
|
36
|
-
built_in = BUILT_IN_TOOLS[tool]
|
|
37
|
-
return built_in.is_a?(Proc) ? built_in.call([]) : built_in
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Standard function tool
|
|
41
|
-
parameters_schema = parameters_schema_for(tool)
|
|
42
|
-
|
|
43
|
-
definition = {
|
|
44
|
-
type: 'function',
|
|
45
|
-
name: tool.name,
|
|
46
|
-
description: tool.description,
|
|
47
|
-
parameters: parameters_schema
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
# Add strict mode if schema supports it
|
|
51
|
-
definition[:strict] = true if parameters_schema['additionalProperties'] == false
|
|
52
|
-
|
|
53
|
-
return definition if tool.respond_to?(:provider_params) && tool.provider_params.empty?
|
|
54
|
-
|
|
55
|
-
if tool.respond_to?(:provider_params) && tool.provider_params.any?
|
|
56
|
-
RubyLLM::Utils.deep_merge(definition, tool.provider_params)
|
|
57
|
-
else
|
|
58
|
-
definition
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def parameters_schema_for(tool)
|
|
63
|
-
if tool.respond_to?(:params_schema) && tool.params_schema
|
|
64
|
-
tool.params_schema
|
|
65
|
-
elsif tool.respond_to?(:parameters)
|
|
66
|
-
schema_from_parameters(tool.parameters)
|
|
67
|
-
else
|
|
68
|
-
EMPTY_PARAMETERS_SCHEMA
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def schema_from_parameters(parameters)
|
|
73
|
-
return EMPTY_PARAMETERS_SCHEMA if parameters.nil? || parameters.empty?
|
|
74
|
-
|
|
75
|
-
if defined?(RubyLLM::Tool::SchemaDefinition)
|
|
76
|
-
schema_definition = RubyLLM::Tool::SchemaDefinition.from_parameters(parameters)
|
|
77
|
-
schema_definition&.json_schema || EMPTY_PARAMETERS_SCHEMA
|
|
78
|
-
else
|
|
79
|
-
# Fallback for older RubyLLM versions
|
|
80
|
-
build_schema_from_parameters(parameters)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def build_schema_from_parameters(parameters)
|
|
85
|
-
properties = {}
|
|
86
|
-
required = []
|
|
87
|
-
|
|
88
|
-
parameters.each do |name, param|
|
|
89
|
-
properties[name.to_s] = {
|
|
90
|
-
type: param.type || 'string',
|
|
91
|
-
description: param.description
|
|
92
|
-
}.compact
|
|
93
|
-
|
|
94
|
-
required << name.to_s if param.required
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
{
|
|
98
|
-
'type' => 'object',
|
|
99
|
-
'properties' => properties,
|
|
100
|
-
'required' => required,
|
|
101
|
-
'additionalProperties' => false
|
|
102
|
-
}
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def format_tool_calls(tool_calls)
|
|
106
|
-
return nil unless tool_calls&.any?
|
|
107
|
-
|
|
108
|
-
tool_calls.map do |_, tc|
|
|
109
|
-
{
|
|
110
|
-
type: 'function_call',
|
|
111
|
-
call_id: tc.id,
|
|
112
|
-
name: tc.name,
|
|
113
|
-
arguments: tc.arguments.is_a?(String) ? tc.arguments : JSON.generate(tc.arguments)
|
|
114
|
-
}
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def parse_tool_calls(tool_calls, parse_arguments: true)
|
|
119
|
-
return nil unless tool_calls&.any?
|
|
120
|
-
|
|
121
|
-
tool_calls.to_h do |tc|
|
|
122
|
-
call_id = tc['call_id'] || tc['id']
|
|
123
|
-
[
|
|
124
|
-
call_id,
|
|
125
|
-
ToolCall.new(
|
|
126
|
-
id: call_id,
|
|
127
|
-
name: tc['name'],
|
|
128
|
-
arguments: if parse_arguments
|
|
129
|
-
parse_tool_call_arguments(tc)
|
|
130
|
-
else
|
|
131
|
-
tc['arguments']
|
|
132
|
-
end
|
|
133
|
-
)
|
|
134
|
-
]
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def parse_tool_call_arguments(tool_call)
|
|
139
|
-
arguments = tool_call['arguments']
|
|
140
|
-
|
|
141
|
-
if arguments.nil? || arguments.empty?
|
|
142
|
-
{}
|
|
143
|
-
elsif arguments.is_a?(Hash)
|
|
144
|
-
arguments
|
|
145
|
-
else
|
|
146
|
-
JSON.parse(arguments)
|
|
147
|
-
end
|
|
148
|
-
rescue JSON::ParserError
|
|
149
|
-
{ raw: arguments }
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# Helper to create built-in tool configurations
|
|
153
|
-
def web_search_tool(search_context_size: nil, user_location: nil, preview: false)
|
|
154
|
-
tool = { type: preview ? 'web_search_preview' : 'web_search' }
|
|
155
|
-
tool[:search_context_size] = search_context_size if search_context_size
|
|
156
|
-
tool[:user_location] = user_location if user_location
|
|
157
|
-
tool
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def web_search_preview_tool(search_context_size: nil, user_location: nil)
|
|
161
|
-
tool = { type: 'web_search_preview' }
|
|
162
|
-
tool[:search_context_size] = search_context_size if search_context_size
|
|
163
|
-
tool[:user_location] = user_location if user_location
|
|
164
|
-
tool
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def file_search_tool(vector_store_ids:, max_num_results: nil, ranking_options: nil)
|
|
168
|
-
tool = {
|
|
169
|
-
type: 'file_search',
|
|
170
|
-
vector_store_ids: Array(vector_store_ids)
|
|
171
|
-
}
|
|
172
|
-
tool[:max_num_results] = max_num_results if max_num_results
|
|
173
|
-
tool[:ranking_options] = ranking_options if ranking_options
|
|
174
|
-
tool
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def code_interpreter_tool(container_type: 'auto')
|
|
178
|
-
{
|
|
179
|
-
type: 'code_interpreter',
|
|
180
|
-
container: { type: container_type }
|
|
181
|
-
}
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def image_generation_tool(partial_images: nil)
|
|
185
|
-
tool = { type: 'image_generation' }
|
|
186
|
-
tool[:partial_images] = partial_images if partial_images
|
|
187
|
-
tool
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def mcp_tool(server_label:, server_url:, require_approval: 'never', allowed_tools: nil, headers: nil)
|
|
191
|
-
tool = {
|
|
192
|
-
type: 'mcp',
|
|
193
|
-
server_label: server_label,
|
|
194
|
-
server_url: server_url,
|
|
195
|
-
require_approval: require_approval
|
|
196
|
-
}
|
|
197
|
-
tool[:allowed_tools] = allowed_tools if allowed_tools
|
|
198
|
-
tool[:headers] = headers if headers
|
|
199
|
-
tool
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
def shell_tool(environment_type: 'container_auto', container_id: nil,
|
|
203
|
-
network_policy: nil, memory_limit: nil)
|
|
204
|
-
env = if container_id
|
|
205
|
-
{ type: 'container_reference', container_id: container_id }
|
|
206
|
-
else
|
|
207
|
-
{ type: environment_type }
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
env[:network_policy] = network_policy if network_policy
|
|
211
|
-
env[:memory_limit] = memory_limit if memory_limit
|
|
212
|
-
|
|
213
|
-
{ type: 'shell', environment: env }
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def apply_patch_tool
|
|
217
|
-
{ type: 'apply_patch' }
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
end
|