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,267 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Providers
5
- class OpenAIResponses
6
- # Model capabilities for OpenAI Responses API models.
7
- # Defines which models support which features.
8
- module Capabilities
9
- module_function
10
-
11
- # Models that support the Responses API
12
- RESPONSES_API_MODELS = %w[
13
- gpt-5.5
14
- gpt-5.2 gpt-5.1 gpt-5.1-codex-max gpt-5.1-codex gpt-5.1-codex-mini gpt-5.1-chat
15
- gpt-5 gpt-5-pro gpt-5-mini gpt-5-nano
16
- gpt-4o gpt-4o-mini gpt-4o-2024-05-13 gpt-4o-2024-08-06 gpt-4o-2024-11-20
17
- gpt-4o-mini-2024-07-18
18
- gpt-4.1 gpt-4.1-mini gpt-4.1-nano
19
- gpt-4-turbo gpt-4-turbo-2024-04-09 gpt-4-turbo-preview
20
- o1 o1-mini o1-preview o1-2024-12-17
21
- o3 o3-mini o4-mini
22
- chatgpt-4o-latest
23
- ].freeze
24
-
25
- # Models with vision capabilities
26
- VISION_MODELS = %w[
27
- gpt-5.5
28
- gpt-5.2 gpt-5.1 gpt-5.1-chat gpt-5 gpt-5-pro gpt-5-mini gpt-5-nano
29
- gpt-4o gpt-4o-mini gpt-4o-2024-05-13 gpt-4o-2024-08-06 gpt-4o-2024-11-20
30
- gpt-4o-mini-2024-07-18
31
- gpt-4.1 gpt-4.1-mini gpt-4.1-nano
32
- gpt-4-turbo gpt-4-turbo-2024-04-09
33
- o1 o3 o4-mini
34
- chatgpt-4o-latest
35
- ].freeze
36
-
37
- # Reasoning models (o-series)
38
- REASONING_MODELS = %w[
39
- gpt-5.5 gpt-5.2 gpt-5.1 gpt-5.1-codex-max gpt-5.1-codex gpt-5.1-codex-mini
40
- gpt-5 gpt-5-pro gpt-5-mini gpt-5-nano
41
- o1 o1-mini o1-preview o1-2024-12-17 o3 o3-mini o4-mini
42
- ].freeze
43
-
44
- # Models that support web search
45
- WEB_SEARCH_MODELS = %w[
46
- gpt-5.5 gpt-5.2 gpt-5.1 gpt-5.1-codex-max gpt-5 gpt-5-pro gpt-5-mini
47
- gpt-4o gpt-4o-mini gpt-4.1 gpt-4.1-mini gpt-4.1-nano
48
- o1 o3 o3-mini o4-mini
49
- ].freeze
50
-
51
- # Models that support code interpreter
52
- CODE_INTERPRETER_MODELS = %w[
53
- gpt-5.5 gpt-5.2 gpt-5.1 gpt-5 gpt-5-pro gpt-5-mini
54
- gpt-4o gpt-4o-mini gpt-4.1 gpt-4.1-mini gpt-4.1-nano
55
- o1 o3 o3-mini o4-mini
56
- ].freeze
57
-
58
- # Context windows by model
59
- CONTEXT_WINDOWS = {
60
- 'gpt-5.5' => 1_050_000,
61
- 'gpt-5.2' => 400_000,
62
- 'gpt-5.1' => 400_000,
63
- 'gpt-5.1-codex-max' => 400_000,
64
- 'gpt-5.1-codex' => 400_000,
65
- 'gpt-5.1-codex-mini' => 400_000,
66
- 'gpt-5.1-chat' => 128_000,
67
- 'gpt-5' => 400_000,
68
- 'gpt-5-pro' => 400_000,
69
- 'gpt-5-mini' => 400_000,
70
- 'gpt-5-nano' => 400_000,
71
- 'gpt-4o' => 128_000,
72
- 'gpt-4o-mini' => 128_000,
73
- 'gpt-4o-2024-05-13' => 128_000,
74
- 'gpt-4o-2024-08-06' => 128_000,
75
- 'gpt-4o-2024-11-20' => 128_000,
76
- 'gpt-4o-mini-2024-07-18' => 128_000,
77
- 'gpt-4.1' => 1_000_000,
78
- 'gpt-4.1-mini' => 1_000_000,
79
- 'gpt-4.1-nano' => 1_000_000,
80
- 'gpt-4-turbo' => 128_000,
81
- 'gpt-4-turbo-2024-04-09' => 128_000,
82
- 'o1' => 200_000,
83
- 'o1-mini' => 128_000,
84
- 'o1-preview' => 128_000,
85
- 'o3' => 200_000,
86
- 'o3-mini' => 200_000,
87
- 'o4-mini' => 200_000
88
- }.freeze
89
-
90
- # Max output tokens by model
91
- MAX_OUTPUT_TOKENS = {
92
- 'gpt-5.5' => 128_000,
93
- 'gpt-5.2' => 128_000,
94
- 'gpt-5.1' => 128_000,
95
- 'gpt-5.1-codex-max' => 128_000,
96
- 'gpt-5.1-codex' => 128_000,
97
- 'gpt-5.1-codex-mini' => 128_000,
98
- 'gpt-5.1-chat' => 16_384,
99
- 'gpt-5' => 128_000,
100
- 'gpt-5-pro' => 128_000,
101
- 'gpt-5-mini' => 128_000,
102
- 'gpt-5-nano' => 128_000,
103
- 'gpt-4o' => 16_384,
104
- 'gpt-4o-mini' => 16_384,
105
- 'gpt-4o-2024-05-13' => 4_096,
106
- 'gpt-4o-2024-08-06' => 16_384,
107
- 'gpt-4o-2024-11-20' => 16_384,
108
- 'gpt-4o-mini-2024-07-18' => 16_384,
109
- 'gpt-4.1' => 32_768,
110
- 'gpt-4.1-mini' => 32_768,
111
- 'gpt-4.1-nano' => 32_768,
112
- 'gpt-4-turbo' => 4_096,
113
- 'o1' => 100_000,
114
- 'o1-mini' => 65_536,
115
- 'o3' => 100_000,
116
- 'o3-mini' => 100_000,
117
- 'o4-mini' => 100_000
118
- }.freeze
119
-
120
- # Pricing per million tokens (as of late 2024)
121
- PRICING = {
122
- 'gpt-4o' => { input: 2.50, output: 10.00, cached_input: 1.25 },
123
- 'gpt-4o-mini' => { input: 0.15, output: 0.60, cached_input: 0.075 },
124
- 'gpt-4.1' => { input: 2.00, output: 8.00, cached_input: 0.50 },
125
- 'gpt-4.1-mini' => { input: 0.40, output: 1.60, cached_input: 0.10 },
126
- 'gpt-4.1-nano' => { input: 0.10, output: 0.40, cached_input: 0.025 },
127
- 'o1' => { input: 15.00, output: 60.00, cached_input: 7.50 },
128
- 'o1-mini' => { input: 1.10, output: 4.40, cached_input: 0.55 },
129
- 'o3' => { input: 10.00, output: 40.00, cached_input: 2.50 },
130
- 'o3-mini' => { input: 1.10, output: 4.40, cached_input: 0.275 },
131
- 'o4-mini' => { input: 1.10, output: 4.40, cached_input: 0.275 }
132
- }.freeze
133
-
134
- def supports_responses_api?(model_id)
135
- model_matches?(model_id, RESPONSES_API_MODELS)
136
- end
137
-
138
- def supports_vision?(model_id)
139
- model_matches?(model_id, VISION_MODELS)
140
- end
141
-
142
- def supports_functions?(model_id)
143
- supports_responses_api?(model_id)
144
- end
145
-
146
- def supports_structured_output?(model_id)
147
- supports_responses_api?(model_id)
148
- end
149
-
150
- def supports_web_search?(model_id)
151
- model_matches?(model_id, WEB_SEARCH_MODELS)
152
- end
153
-
154
- def supports_code_interpreter?(model_id)
155
- model_matches?(model_id, CODE_INTERPRETER_MODELS)
156
- end
157
-
158
- def supports_tool_parallel_control?(_model_id)
159
- true
160
- end
161
-
162
- def reasoning_model?(model_id)
163
- model_matches?(model_id, REASONING_MODELS)
164
- end
165
-
166
- def context_window_for(model_id)
167
- find_capability(model_id, CONTEXT_WINDOWS) || 128_000
168
- end
169
-
170
- def max_tokens_for(model_id)
171
- find_capability(model_id, MAX_OUTPUT_TOKENS) || 16_384
172
- end
173
-
174
- def input_price_for(model_id)
175
- pricing = find_capability(model_id, PRICING)
176
- pricing ? pricing[:input] : 0.0
177
- end
178
-
179
- def output_price_for(model_id)
180
- pricing = find_capability(model_id, PRICING)
181
- pricing ? pricing[:output] : 0.0
182
- end
183
-
184
- def pricing_for(model_id)
185
- pricing = find_capability(model_id, PRICING) || { input: 0.0, output: 0.0 }
186
- {
187
- text_tokens: {
188
- standard: {
189
- input_per_million: pricing[:input],
190
- output_per_million: pricing[:output],
191
- cached_input_per_million: pricing[:cached_input] || (pricing[:input] / 2)
192
- }
193
- }
194
- }
195
- end
196
-
197
- def modalities_for(model_id)
198
- input = ['text']
199
- input << 'image' if supports_vision?(model_id)
200
-
201
- {
202
- input: input,
203
- output: ['text']
204
- }
205
- end
206
-
207
- def capabilities_for(model_id)
208
- caps = %w[streaming function_calling structured_output]
209
- caps << 'vision' if supports_vision?(model_id)
210
- caps << 'web_search' if supports_web_search?(model_id)
211
- caps << 'code_interpreter' if supports_code_interpreter?(model_id)
212
- caps << 'reasoning' if reasoning_model?(model_id)
213
- caps
214
- end
215
-
216
- def model_family(model_id)
217
- case model_id
218
- when /^gpt-5\.5/ then 'gpt-5.5'
219
- when /^gpt-5\.2/ then 'gpt-5.2'
220
- when /^gpt-5\.1/ then 'gpt-5.1'
221
- when /^gpt-5/ then 'gpt-5'
222
- when /^gpt-4\.1/ then 'gpt-4.1'
223
- when /^gpt-4o-mini/ then 'gpt-4o-mini'
224
- when /^gpt-4o/ then 'gpt-4o'
225
- when /^gpt-4-turbo/ then 'gpt-4-turbo'
226
- when /^o1/ then 'o1'
227
- when /^o3/ then 'o3'
228
- when /^o4/ then 'o4'
229
- else 'other'
230
- end
231
- end
232
-
233
- def format_display_name(model_id)
234
- model_id
235
- .gsub(/[-_]/, ' ')
236
- .split
237
- .map(&:capitalize)
238
- .join(' ')
239
- end
240
-
241
- # Temperature is not supported for reasoning models
242
- def normalize_temperature(temperature, model_id)
243
- return nil if reasoning_model?(model_id)
244
-
245
- temperature
246
- end
247
-
248
- private_class_method def find_capability(model_id, mapping)
249
- # Direct match
250
- return mapping[model_id] if mapping.key?(model_id)
251
-
252
- # Try base model name (without date suffix)
253
- base_model = model_id.gsub(/-\d{4}-\d{2}-\d{2}$/, '')
254
- return mapping[base_model] if mapping.key?(base_model)
255
-
256
- nil
257
- end
258
-
259
- private_class_method def model_matches?(model_id, model_list)
260
- model_list.any? do |pattern|
261
- model_id == pattern || model_id.start_with?("#{pattern}-")
262
- end
263
- end
264
- end
265
- end
266
- end
267
- end
@@ -1,273 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Providers
5
- class OpenAIResponses
6
- # Chat completion methods for the OpenAI Responses API.
7
- # Handles converting RubyLLM messages to Responses API format and parsing responses.
8
- module Chat
9
- def completion_url
10
- 'responses'
11
- end
12
-
13
- module_function
14
-
15
- # rubocop:disable Metrics/ParameterLists
16
- def render_payload(messages, tools:, temperature:, model:, stream: false,
17
- schema: nil, thinking: nil, tool_prefs: nil) # rubocop:disable Lint/UnusedMethodArgument
18
- tool_prefs ||= {}
19
- system_messages, non_system_messages = messages.partition { |m| m.role == :system }
20
-
21
- instructions = system_messages.map { |m| extract_text_content(m.content) }.join("\n\n")
22
-
23
- last_response_id = extract_last_response_id(messages)
24
- input_messages = unchained_messages(non_system_messages, last_response_id)
25
-
26
- payload = {
27
- model: model.id,
28
- input: format_input(input_messages),
29
- stream: stream
30
- }
31
-
32
- payload[:instructions] = instructions unless instructions.empty?
33
- payload[:temperature] = temperature unless temperature.nil?
34
- apply_tools(payload, tools, tool_prefs)
35
- payload[:text] = build_schema_format(schema) if schema
36
- payload[:previous_response_id] = last_response_id if last_response_id
37
-
38
- payload
39
- end
40
- # rubocop:enable Metrics/ParameterLists
41
-
42
- # Apply tools and tool preferences to the payload.
43
- def apply_tools(payload, tools, tool_prefs)
44
- return unless tools.any?
45
-
46
- payload[:tools] = tools.map { |_, tool| Tools.tool_for(tool) }
47
- payload[:tool_choice] = build_tool_choice(tool_prefs[:choice]) unless tool_prefs[:choice].nil?
48
- payload[:parallel_tool_calls] = tool_prefs[:calls] == :many unless tool_prefs[:calls].nil?
49
- end
50
-
51
- # Convert a RubyLLM tool choice symbol to the Responses API format.
52
- # Responses API accepts "auto", "required", "none", or
53
- # { type: "function", name: "fn_name" } for a specific function.
54
- def build_tool_choice(choice)
55
- case choice
56
- when :auto, :none, :required
57
- choice.to_s
58
- else
59
- { type: 'function', name: choice.to_s }
60
- end
61
- end
62
-
63
- # Build the Responses API text format block from a schema.
64
- # Schema arrives pre-normalized as { name:, schema:, strict: } from
65
- # RubyLLM::Chat.with_schema (v1.13+), or as a raw hash (legacy).
66
- def build_schema_format(schema)
67
- schema_name = schema[:name] || 'response'
68
- schema_def = schema[:schema] || schema
69
- strict = schema.key?(:strict) ? schema[:strict] : true
70
-
71
- {
72
- format: {
73
- type: 'json_schema',
74
- name: schema_name,
75
- schema: schema_def,
76
- strict: strict
77
- }
78
- }
79
- end
80
-
81
- def extract_last_response_id(messages)
82
- messages
83
- .select { |m| m.role == :assistant && m.respond_to?(:response_id) }
84
- .map(&:response_id)
85
- .compact
86
- .last
87
- end
88
-
89
- # When chaining via previous_response_id, the API expects only the new
90
- # items in `input` -- the rest already lives in the server-side response
91
- # chain. Sending the full history every turn appends it to that chain
92
- # and causes O(N^2) input_tokens growth. See issue #10.
93
- def unchained_messages(messages, last_response_id)
94
- return messages unless last_response_id
95
-
96
- anchor = messages.rindex do |m|
97
- m.role == :assistant && m.respond_to?(:response_id) && m.response_id == last_response_id
98
- end
99
- return messages unless anchor
100
-
101
- messages[(anchor + 1)..] || []
102
- end
103
-
104
- def parse_completion_response(response)
105
- data = response.body
106
- return if data.nil? || data.empty?
107
-
108
- data = JSON.parse(data) if data.is_a?(String)
109
-
110
- raise RubyLLM::Error.new(response, data.dig('error', 'message')) if data.dig('error', 'message')
111
-
112
- output = data['output'] || []
113
-
114
- # Extract text content from output
115
- content = extract_output_text(output)
116
-
117
- # Extract tool calls from function_call outputs
118
- tool_calls = extract_tool_calls(output)
119
-
120
- usage = data['usage'] || {}
121
- cached_tokens = usage.dig('input_tokens_details', 'cached_tokens')
122
-
123
- Message.new(
124
- role: :assistant,
125
- content: content,
126
- tool_calls: tool_calls,
127
- input_tokens: usage['input_tokens'],
128
- output_tokens: usage['output_tokens'],
129
- cached_tokens: cached_tokens,
130
- cache_creation_tokens: 0,
131
- model_id: data['model'],
132
- response_id: data['id'],
133
- built_in_tool_events: BuiltInTools.extract_events(output),
134
- raw: response
135
- )
136
- end
137
-
138
- def format_input(messages) # rubocop:disable Metrics/MethodLength
139
- result = []
140
-
141
- messages.each do |msg|
142
- if msg.tool_call_id
143
- # Tool result message - function_call_output type
144
- result << {
145
- type: 'function_call_output',
146
- call_id: msg.tool_call_id,
147
- output: extract_text_content(msg.content)
148
- }
149
- elsif msg.tool_calls&.any?
150
- # Assistant message with tool calls
151
- # First add any text content as a message
152
- text = extract_text_content(msg.content)
153
- if text && !text.empty?
154
- result << {
155
- type: 'message',
156
- role: 'assistant',
157
- content: text
158
- }
159
- end
160
-
161
- # Then add each function call as a separate item
162
- msg.tool_calls.each_value do |tc|
163
- result << {
164
- type: 'function_call',
165
- call_id: tc.id,
166
- name: tc.name,
167
- arguments: tc.arguments.is_a?(String) ? tc.arguments : JSON.generate(tc.arguments)
168
- }
169
- end
170
- else
171
- # Regular message
172
- result << {
173
- type: 'message',
174
- role: format_role(msg.role),
175
- content: format_message_content(msg.content, nil)
176
- }
177
- end
178
- end
179
-
180
- result
181
- end
182
-
183
- def format_message_content(content, tool_calls = nil)
184
- parts = []
185
-
186
- # Add text content
187
- text = extract_text_content(content)
188
- parts << { type: 'input_text', text: text } if text && !text.empty?
189
-
190
- # Add attachments if present
191
- if content.is_a?(RubyLLM::Content)
192
- content.attachments.each do |attachment|
193
- parts << Media.format_attachment(attachment)
194
- end
195
- end
196
-
197
- # Add tool calls if present (for assistant messages)
198
- if tool_calls&.any?
199
- tool_calls.each_value do |tc|
200
- parts << {
201
- type: 'function_call',
202
- call_id: tc.id,
203
- name: tc.name,
204
- arguments: tc.arguments.is_a?(String) ? tc.arguments : JSON.generate(tc.arguments)
205
- }
206
- end
207
- end
208
-
209
- # Return simple text for single text content
210
- return parts.first[:text] if parts.length == 1 && parts.first[:type] == 'input_text'
211
-
212
- parts
213
- end
214
-
215
- def extract_text_content(content)
216
- case content
217
- when String
218
- content
219
- when RubyLLM::Content
220
- content.text
221
- when Hash
222
- content[:text] || content['text']
223
- else
224
- content.to_s
225
- end
226
- end
227
-
228
- def format_role(role)
229
- case role
230
- when :system then 'developer'
231
- when :assistant then 'assistant'
232
- when :tool then 'user' # Tool results come from user perspective
233
- else role.to_s
234
- end
235
- end
236
-
237
- def extract_output_text(output)
238
- output
239
- .select { |item| item['type'] == 'message' }
240
- .flat_map { |item| item['content'] || [] }
241
- .select { |c| c['type'] == 'output_text' }
242
- .map { |c| c['text'] }
243
- .join
244
- end
245
-
246
- def extract_tool_calls(output)
247
- function_calls = output.select { |item| item['type'] == 'function_call' }
248
- return nil if function_calls.empty?
249
-
250
- function_calls.to_h do |fc|
251
- [
252
- fc['call_id'],
253
- ToolCall.new(
254
- id: fc['call_id'],
255
- name: fc['name'],
256
- arguments: parse_arguments(fc['arguments'])
257
- )
258
- ]
259
- end
260
- end
261
-
262
- def parse_arguments(arguments)
263
- return {} if arguments.nil? || arguments.empty?
264
- return arguments if arguments.is_a?(Hash)
265
-
266
- JSON.parse(arguments)
267
- rescue JSON::ParserError
268
- { raw: arguments }
269
- end
270
- end
271
- end
272
- end
273
- end
@@ -1,95 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Providers
5
- class OpenAIResponses
6
- # Media handling methods for the OpenAI Responses API.
7
- # Handles images, audio, PDFs, and other file types.
8
- module Media
9
- module_function
10
-
11
- def format_attachment(attachment)
12
- case attachment.type
13
- when :image
14
- format_image(attachment)
15
- when :pdf
16
- format_pdf(attachment)
17
- when :audio
18
- format_audio(attachment)
19
- when :text
20
- format_text_file(attachment)
21
- else
22
- raise UnsupportedAttachmentError, attachment.type
23
- end
24
- end
25
-
26
- def format_image(image)
27
- if image.url?
28
- {
29
- type: 'input_image',
30
- image_url: image.source
31
- }
32
- else
33
- {
34
- type: 'input_image',
35
- image_url: image.for_llm
36
- }
37
- end
38
- end
39
-
40
- def format_pdf(pdf)
41
- {
42
- type: 'input_file',
43
- filename: extract_filename(pdf.source),
44
- file_data: pdf.for_llm
45
- }
46
- end
47
-
48
- def format_audio(audio)
49
- {
50
- type: 'input_audio',
51
- data: audio.for_llm,
52
- format: detect_audio_format(audio.source)
53
- }
54
- end
55
-
56
- def format_text_file(text_file)
57
- {
58
- type: 'input_text',
59
- text: text_file.for_llm
60
- }
61
- end
62
-
63
- def extract_filename(source)
64
- return 'file' unless source
65
-
66
- if source.respond_to?(:path)
67
- File.basename(source.path)
68
- else
69
- File.basename(source.to_s)
70
- end
71
- end
72
-
73
- def detect_audio_format(source)
74
- return 'mp3' unless source
75
-
76
- ext = if source.respond_to?(:path)
77
- File.extname(source.path)
78
- else
79
- File.extname(source.to_s)
80
- end
81
-
82
- case ext.downcase
83
- when '.mp3' then 'mp3'
84
- when '.wav' then 'wav'
85
- when '.webm' then 'webm'
86
- when '.ogg' then 'ogg'
87
- when '.flac' then 'flac'
88
- when '.m4a' then 'm4a'
89
- else 'mp3'
90
- end
91
- end
92
- end
93
- end
94
- end
95
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Providers
5
- class OpenAIResponses
6
- # Model listing methods for the OpenAI Responses API.
7
- module Models
8
- module_function
9
-
10
- def models_url
11
- 'models'
12
- end
13
-
14
- def parse_list_models_response(response, slug, capabilities)
15
- models_data = response.body
16
- models_data = models_data['data'] if models_data.is_a?(Hash) && models_data['data']
17
-
18
- Array(models_data).filter_map do |model_data|
19
- model_id = model_data['id']
20
-
21
- # Only include models that support the Responses API
22
- next unless capabilities.supports_responses_api?(model_id)
23
-
24
- Model::Info.new(
25
- id: model_id,
26
- name: capabilities.format_display_name(model_id),
27
- provider: slug,
28
- family: capabilities.model_family(model_id),
29
- context_window: capabilities.context_window_for(model_id),
30
- max_output_tokens: capabilities.max_tokens_for(model_id),
31
- modalities: capabilities.modalities_for(model_id),
32
- capabilities: capabilities.capabilities_for(model_id),
33
- pricing: capabilities.pricing_for(model_id),
34
- metadata: {
35
- created_at: model_data['created'] ? Time.at(model_data['created']) : nil,
36
- owned_by: model_data['owned_by'],
37
- supports_responses_api: true,
38
- supports_web_search: capabilities.supports_web_search?(model_id),
39
- supports_code_interpreter: capabilities.supports_code_interpreter?(model_id),
40
- reasoning_model: capabilities.reasoning_model?(model_id)
41
- }.compact
42
- )
43
- end
44
- end
45
- end
46
- end
47
- end
48
- end