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.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/MIGRATION.md +276 -0
  4. data/README.md +42 -32
  5. data/lib/ruby_llm/providers/openai_responses/active_record_extension.rb +12 -109
  6. data/lib/ruby_llm/providers/openai_responses/base.rb +2 -7
  7. data/lib/ruby_llm/providers/openai_responses/batch.rb +8 -8
  8. data/lib/ruby_llm/providers/openai_responses/batches.rb +6 -14
  9. data/lib/ruby_llm/providers/openai_responses/built_in_tools.rb +1 -1
  10. data/lib/ruby_llm/providers/openai_responses/chat_extension.rb +3 -12
  11. data/lib/ruby_llm/providers/openai_responses/compaction.rb +1 -1
  12. data/lib/ruby_llm/providers/openai_responses/message_extension.rb +11 -21
  13. data/lib/ruby_llm/providers/openai_responses/model_registry.rb +17 -261
  14. data/lib/ruby_llm/providers/openai_responses/protocol.rb +103 -0
  15. data/lib/ruby_llm/providers/openai_responses/stream_accumulator.rb +48 -0
  16. data/lib/ruby_llm/providers/openai_responses/web_socket.rb +51 -77
  17. data/lib/ruby_llm/providers/openai_responses.rb +23 -80
  18. data/lib/rubyllm_responses_api.rb +13 -46
  19. metadata +17 -18
  20. data/lib/ruby_llm/providers/openai_responses/capabilities.rb +0 -267
  21. data/lib/ruby_llm/providers/openai_responses/chat.rb +0 -273
  22. data/lib/ruby_llm/providers/openai_responses/media.rb +0 -95
  23. data/lib/ruby_llm/providers/openai_responses/models.rb +0 -48
  24. data/lib/ruby_llm/providers/openai_responses/stream_accumulator_extension.rb +0 -39
  25. data/lib/ruby_llm/providers/openai_responses/streaming.rb +0 -140
  26. 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