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
@@ -3,133 +3,36 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class OpenAIResponses
6
- # Extends RubyLLM's ActiveRecord MessageMethods to support response_id persistence
7
- # for stateful conversations with the OpenAI Responses API.
8
- #
9
- # This is automatically applied when Rails loads ActiveRecord.
10
- # Just add a migration: add_column :messages, :response_id, :string
11
- #
12
-
13
- # Extension for the NEW MessageMethods (RubyLLM 2.0+)
6
+ # Keep native persistence for tools, usage, attachments and reasoning.
14
7
  module MessageMethodsExtension
15
8
  def to_llm
16
- cached = has_attribute?(:cached_tokens) ? self[:cached_tokens] : nil
17
- cache_creation = has_attribute?(:cache_creation_tokens) ? self[:cache_creation_tokens] : nil
18
- resp_id = has_attribute?(:response_id) ? self[:response_id] : nil
19
-
20
- RubyLLM::Message.new(
21
- role: role.to_sym,
22
- content: extract_content,
23
- tool_calls: extract_tool_calls,
24
- tool_call_id: extract_tool_call_id,
25
- input_tokens: input_tokens,
26
- output_tokens: output_tokens,
27
- cached_tokens: cached,
28
- cache_creation_tokens: cache_creation,
29
- model_id: model_association&.model_id,
30
- response_id: resp_id
31
- )
9
+ message = super
10
+ message.response_id = self[:response_id] if has_attribute?(:response_id)
11
+ message
32
12
  end
33
13
  end
34
14
 
35
- # Extension for the NEW ChatMethods (RubyLLM 2.0+)
36
15
  module ChatMethodsExtension
37
- def persist_message_completion(message)
38
- super
39
- save_response_id(message)
40
- end
41
-
42
16
  private
43
17
 
44
- def save_response_id(message)
45
- return unless message
46
- return unless message.respond_to?(:response_id) && message.response_id
47
- return unless @message.has_attribute?(:response_id)
48
-
49
- @message.update_column(:response_id, message.response_id)
50
- end
51
- end
52
-
53
- # Extension for LEGACY MessageLegacyMethods (RubyLLM 1.x)
54
- module MessageLegacyMethodsExtension
55
- def to_llm
56
- resp_id = has_attribute?(:response_id) ? self[:response_id] : nil
57
-
58
- RubyLLM::Message.new(
59
- role: role.to_sym,
60
- content: extract_content,
61
- tool_calls: extract_tool_calls,
62
- tool_call_id: extract_tool_call_id,
63
- input_tokens: input_tokens,
64
- output_tokens: output_tokens,
65
- model_id: model_id,
66
- response_id: resp_id
67
- )
18
+ def message_attributes(message)
19
+ attributes = super
20
+ assign_supported_attribute(attributes, :response_id, message.response_id)
21
+ attributes
68
22
  end
69
23
  end
70
24
 
71
- # Extension for LEGACY ChatLegacyMethods (RubyLLM 1.x)
72
- module ChatLegacyMethodsExtension
73
- def persist_message_completion(message)
74
- super
75
- save_response_id_legacy(message)
76
- end
77
-
78
- private
79
-
80
- def save_response_id_legacy(message)
81
- return unless message
82
- return unless message.respond_to?(:response_id) && message.response_id
83
- return unless @message.has_attribute?(:response_id)
84
-
85
- @message.update_column(:response_id, message.response_id)
86
- end
87
- end
88
-
89
- @active_record_extensions_applied = false
90
-
91
- # Apply ActiveRecord extensions for response_id persistence.
92
- # Called automatically when ActiveRecord loads, or can be called manually.
93
25
  def self.apply_active_record_extensions!
94
- return if @active_record_extensions_applied
95
-
96
- applied = false
97
-
98
- # Try to apply to NEW modules (RubyLLM 2.0+)
99
- begin
100
- require 'ruby_llm/active_record/message_methods'
101
- require 'ruby_llm/active_record/chat_methods'
102
-
103
- RubyLLM::ActiveRecord::MessageMethods.prepend(MessageMethodsExtension)
104
- RubyLLM::ActiveRecord::ChatMethods.prepend(ChatMethodsExtension)
105
- applied = true
106
- rescue LoadError, NameError
107
- # New modules not available
108
- end
109
-
110
- # Try to apply to LEGACY modules (RubyLLM 1.x)
111
- begin
112
- require 'ruby_llm/active_record/acts_as_legacy'
113
-
114
- if defined?(RubyLLM::ActiveRecord::MessageLegacyMethods)
115
- RubyLLM::ActiveRecord::MessageLegacyMethods.prepend(MessageLegacyMethodsExtension)
116
- end
117
-
118
- if defined?(RubyLLM::ActiveRecord::ChatLegacyMethods)
119
- RubyLLM::ActiveRecord::ChatLegacyMethods.prepend(ChatLegacyMethodsExtension)
120
- end
121
- applied = true
122
- rescue LoadError, NameError
123
- # Legacy modules not available
124
- end
26
+ require 'ruby_llm/active_record/message_methods'
27
+ require 'ruby_llm/active_record/chat_methods'
125
28
 
126
- @active_record_extensions_applied = applied
29
+ RubyLLM::ActiveRecord::MessageMethods.prepend(MessageMethodsExtension)
30
+ RubyLLM::ActiveRecord::ChatMethods.prepend(ChatMethodsExtension)
127
31
  end
128
32
  end
129
33
  end
130
34
  end
131
35
 
132
- # Auto-apply extensions when ActiveRecord is loaded in Rails
133
36
  if defined?(ActiveSupport.on_load)
134
37
  ActiveSupport.on_load(:active_record) do
135
38
  RubyLLM::Providers::OpenAIResponses.apply_active_record_extensions!
@@ -2,13 +2,8 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Providers
5
- # OpenAI Responses API provider for RubyLLM.
6
- # Implements the new Responses API which provides built-in tools,
7
- # stateful conversations, background mode, and MCP support.
8
- #
9
- # This base file defines the class structure before modules are loaded
10
- # to avoid "superclass mismatch" errors.
11
- class OpenAIResponses < Provider
5
+ # OpenAI's native provider with opt-in server-side conversation state.
6
+ class OpenAIResponses < OpenAI
12
7
  end
13
8
  end
14
9
  end
@@ -10,7 +10,7 @@ module RubyLLM
10
10
  # behind a clean Ruby API that mirrors RubyLLM::Chat.
11
11
  #
12
12
  # @example
13
- # batch = RubyLLM.batch(model: 'gpt-4o', provider: :openai_responses)
13
+ # batch = RubyLLM::ResponsesAPI.batch(model: 'gpt-4o', provider: :openai_responses)
14
14
  # batch.add("What is Ruby?")
15
15
  # batch.add("What is Python?", instructions: "Be brief")
16
16
  # batch.create!
@@ -60,8 +60,8 @@ module RubyLLM
60
60
  # @param metadata [Hash, nil] Optional metadata for the batch
61
61
  # @return [self]
62
62
  def create!(metadata: nil)
63
- raise Error.new(nil, 'No requests added') if @requests.empty?
64
- raise Error.new(nil, 'Batch already created') if @id
63
+ raise Error, 'No requests added' if @requests.empty?
64
+ raise Error, 'Batch already created' if @id
65
65
 
66
66
  jsonl = Batches.build_jsonl(@requests)
67
67
  file_id = upload_file(jsonl)
@@ -82,7 +82,7 @@ module RubyLLM
82
82
  # Fetch the latest batch status from the API.
83
83
  # @return [self]
84
84
  def refresh!
85
- raise Error.new(nil, 'Batch not yet created') unless @id
85
+ raise Error, 'Batch not yet created' unless @id
86
86
 
87
87
  response = @provider.instance_variable_get(:@connection).get(Batches.batch_url(@id))
88
88
  @data = response.body
@@ -149,7 +149,7 @@ module RubyLLM
149
149
  break if Batches.terminal?(status)
150
150
 
151
151
  if timeout && (Time.now - start_time) > timeout
152
- raise Error.new(nil, "Batch polling timeout after #{timeout} seconds")
152
+ raise Error, "Batch polling timeout after #{timeout} seconds"
153
153
  end
154
154
 
155
155
  sleep interval
@@ -162,7 +162,7 @@ module RubyLLM
162
162
  # @return [Hash<String, Message>] Results keyed by custom_id
163
163
  def results
164
164
  output_file_id = @data['output_file_id']
165
- raise Error.new(nil, 'No output file available yet') unless output_file_id
165
+ raise Error, 'No output file available yet' unless output_file_id
166
166
 
167
167
  jsonl = fetch_file_content(output_file_id)
168
168
  Batches.parse_results_to_messages(jsonl)
@@ -181,7 +181,7 @@ module RubyLLM
181
181
  # Cancel the batch.
182
182
  # @return [self]
183
183
  def cancel!
184
- raise Error.new(nil, 'Batch not yet created') unless @id
184
+ raise Error, 'Batch not yet created' unless @id
185
185
 
186
186
  response = @provider.instance_variable_get(:@connection).post(Batches.cancel_batch_url(@id), {})
187
187
  @data = response.body
@@ -195,7 +195,7 @@ module RubyLLM
195
195
  when Symbol, String
196
196
  slug = provider.to_sym
197
197
  provider_class = RubyLLM::Provider.providers[slug]
198
- raise Error.new(nil, "Unknown provider: #{slug}") unless provider_class
198
+ raise Error, "Unknown provider: #{slug}" unless provider_class
199
199
 
200
200
  provider_class.new(RubyLLM.config)
201
201
  else
@@ -96,7 +96,7 @@ module RubyLLM
96
96
  end
97
97
 
98
98
  # Parse JSONL output into a Hash of { custom_id => Message }.
99
- # Reuses Chat.extract_output_text and Chat.extract_tool_calls to avoid duplication.
99
+ # Uses the native Responses parser, including reasoning and server tools.
100
100
  def parse_results_to_messages(jsonl_string)
101
101
  results = parse_results(jsonl_string)
102
102
  results.each_with_object({}) do |result, hash|
@@ -104,19 +104,11 @@ module RubyLLM
104
104
  response_body = result.dig('response', 'body')
105
105
  next unless response_body
106
106
 
107
- output = response_body['output'] || []
108
- content = Chat.extract_output_text(output)
109
- tool_calls = Chat.extract_tool_calls(output)
110
- usage = response_body['usage'] || {}
111
-
112
- hash[custom_id] = Message.new(
113
- role: :assistant,
114
- content: content,
115
- tool_calls: tool_calls,
116
- input_tokens: usage['input_tokens'],
117
- output_tokens: usage['output_tokens'],
118
- model_id: response_body['model']
119
- )
107
+ next if result.dig('response', 'status_code').to_i >= 400
108
+
109
+ raw = Faraday::Response.new(status: result.dig('response', 'status_code') || 200, body: response_body)
110
+ provider = OpenAIResponses.new(RubyLLM.config)
111
+ hash[custom_id] = Protocol.new(provider).parse_completion_body(response_body, raw: raw)
120
112
  end
121
113
  end
122
114
 
@@ -213,7 +213,7 @@ module RubyLLM
213
213
 
214
214
  # Build a list of {tool_call:, result:} events from the response output.
215
215
  # Used to surface server-side built-in tool activity through the standard
216
- # on_tool_call / on_tool_result callbacks (issue #1).
216
+ # before_tool_call / after_tool_result callbacks (issue #1).
217
217
  def extract_events(output)
218
218
  return [] unless output.is_a?(Array)
219
219
 
@@ -3,7 +3,7 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class OpenAIResponses
6
- # Extends RubyLLM::Chat to fire on_tool_call / on_tool_result for built-in
6
+ # Extends RubyLLM::Chat to fire before_tool_call / after_tool_result for built-in
7
7
  # server-side tools (web_search, code_interpreter, file_search, etc.)
8
8
  # carried on the assistant message. This lets users observe built-in tool
9
9
  # activity through the same callback API as locally executed function
@@ -25,19 +25,10 @@ module RubyLLM
25
25
 
26
26
  def dispatch_built_in_tool_events(message)
27
27
  message.built_in_tool_events.each do |event|
28
- fire_callback(:before_tool_call, :tool_call, event[:tool_call])
29
- fire_callback(:after_tool_result, :tool_result, event[:result])
28
+ run_callbacks(:before_tool_call, event[:tool_call])
29
+ run_callbacks(:after_tool_result, event[:result])
30
30
  end
31
31
  end
32
-
33
- # Mirrors RubyLLM::Chat#run_callbacks (private since 1.13): dispatches
34
- # through the new @callbacks array API if present, and always falls
35
- # back to the legacy @on hash so older ruby_llm versions still work.
36
- def fire_callback(new_name, legacy_name, *args)
37
- callbacks = instance_variable_defined?(:@callbacks) ? @callbacks : nil
38
- callbacks[new_name]&.each { |cb| cb.call(*args) } if callbacks
39
- @on[legacy_name]&.call(*args)
40
- end
41
32
  end
42
33
  end
43
34
  end
@@ -10,7 +10,7 @@ module RubyLLM
10
10
 
11
11
  # Build context_management parameter for compaction
12
12
  # @param compact_threshold [Integer] Token count threshold to trigger compaction (minimum 1000)
13
- # @return [Hash] Parameters to merge into request payload via with_params
13
+ # @return [Hash] Parameters to merge into request payload via with_provider_options
14
14
  def compaction_params(compact_threshold: 200_000)
15
15
  {
16
16
  context_management: [
@@ -3,33 +3,23 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class OpenAIResponses
6
- # Extends RubyLLM::Message to support response_id for stateful conversations
7
- # and built_in_tool_events for server-side tool calls (web_search,
8
- # code_interpreter, etc.) that should fire on_tool_call/on_tool_result.
6
+ # Extra state carried by this provider; native Message fields are preserved.
9
7
  module MessageExtension
10
- attr_accessor :response_id, :built_in_tool_events
8
+ attr_accessor :response_id, :built_in_tool_events, :responses_api_body
11
9
 
12
- def self.included(base)
13
- base.class_eval do
14
- alias_method :original_initialize, :initialize
15
-
16
- define_method(:initialize) do |options = {}|
17
- original_initialize(options)
18
- @response_id = options[:response_id]
19
- @built_in_tool_events = options[:built_in_tool_events]
20
- end
21
-
22
- alias_method :original_to_h, :to_h
10
+ def initialize(options = {})
11
+ super
12
+ @response_id = options[:response_id]
13
+ @built_in_tool_events = options[:built_in_tool_events]
14
+ @responses_api_body = options[:responses_api_body]
15
+ end
23
16
 
24
- define_method(:to_h) do
25
- original_to_h.merge(response_id: response_id).compact
26
- end
27
- end
17
+ def to_h
18
+ response_id ? super.merge(response_id: response_id) : super
28
19
  end
29
20
  end
30
21
  end
31
22
  end
32
23
  end
33
24
 
34
- # Apply the extension to RubyLLM::Message
35
- RubyLLM::Message.include(RubyLLM::Providers::OpenAIResponses::MessageExtension)
25
+ RubyLLM::Message.prepend(RubyLLM::Providers::OpenAIResponses::MessageExtension)
@@ -3,273 +3,29 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class OpenAIResponses
6
- # Registers OpenAI Responses API models with RubyLLM
7
- # Models updated May 2026 based on OpenAI documentation
6
+ # Reuse the current OpenAI catalog rather than maintaining model snapshots.
8
7
  module ModelRegistry
9
- MODELS = [
10
- # ===================
11
- # GPT-5.5 Series (Latest flagship - May 2026)
12
- # ===================
13
- {
14
- id: 'gpt-5.5',
15
- name: 'GPT-5.5',
16
- provider: 'openai_responses',
17
- family: 'gpt-5.5',
18
- context_window: 1_050_000,
19
- max_output_tokens: 128_000,
20
- modalities: { input: %w[text image], output: ['text'] },
21
- capabilities: %w[
22
- streaming function_calling structured_output vision reasoning
23
- web_search file_search image_generation code_interpreter shell
24
- apply_patch computer_use mcp
25
- ]
26
- },
8
+ def by_provider(provider)
9
+ own = super
10
+ return own unless provider.to_s == 'openai_responses'
27
11
 
28
- # ===================
29
- # GPT-5.2 Series (Latest flagship - December 2025)
30
- # ===================
31
- {
32
- id: 'gpt-5.2',
33
- name: 'GPT-5.2',
34
- provider: 'openai_responses',
35
- family: 'gpt-5.2',
36
- context_window: 400_000,
37
- max_output_tokens: 128_000,
38
- modalities: { input: %w[text image], output: ['text'] },
39
- capabilities: %w[streaming function_calling structured_output vision reasoning web_search code_interpreter]
40
- },
41
-
42
- # ===================
43
- # GPT-5.1 Series (November 2025)
44
- # ===================
45
- {
46
- id: 'gpt-5.1',
47
- name: 'GPT-5.1',
48
- provider: 'openai_responses',
49
- family: 'gpt-5.1',
50
- context_window: 400_000,
51
- max_output_tokens: 128_000,
52
- modalities: { input: %w[text image], output: ['text'] },
53
- capabilities: %w[streaming function_calling structured_output vision reasoning web_search code_interpreter]
54
- },
55
- {
56
- id: 'gpt-5.1-codex-max',
57
- name: 'GPT-5.1 Codex Max',
58
- provider: 'openai_responses',
59
- family: 'gpt-5.1',
60
- context_window: 400_000,
61
- max_output_tokens: 128_000,
62
- modalities: { input: %w[text image], output: ['text'] },
63
- capabilities: %w[streaming function_calling structured_output reasoning web_search]
64
- },
65
- {
66
- id: 'gpt-5.1-codex',
67
- name: 'GPT-5.1 Codex',
68
- provider: 'openai_responses',
69
- family: 'gpt-5.1',
70
- context_window: 400_000,
71
- max_output_tokens: 128_000,
72
- modalities: { input: %w[text image], output: ['text'] },
73
- capabilities: %w[streaming function_calling structured_output reasoning]
74
- },
75
- {
76
- id: 'gpt-5.1-codex-mini',
77
- name: 'GPT-5.1 Codex Mini',
78
- provider: 'openai_responses',
79
- family: 'gpt-5.1',
80
- context_window: 400_000,
81
- max_output_tokens: 128_000,
82
- modalities: { input: %w[text image], output: ['text'] },
83
- capabilities: %w[streaming function_calling structured_output reasoning]
84
- },
85
- {
86
- id: 'gpt-5.1-chat',
87
- name: 'GPT-5.1 Chat',
88
- provider: 'openai_responses',
89
- family: 'gpt-5.1',
90
- context_window: 128_000,
91
- max_output_tokens: 16_384,
92
- modalities: { input: %w[text image], output: ['text'] },
93
- capabilities: %w[streaming function_calling structured_output vision]
94
- },
95
-
96
- # ===================
97
- # GPT-5 Series (August 2025)
98
- # ===================
99
- {
100
- id: 'gpt-5',
101
- name: 'GPT-5',
102
- provider: 'openai_responses',
103
- family: 'gpt-5',
104
- context_window: 400_000,
105
- max_output_tokens: 128_000,
106
- modalities: { input: %w[text image], output: ['text'] },
107
- capabilities: %w[streaming function_calling structured_output vision reasoning web_search code_interpreter]
108
- },
109
- {
110
- id: 'gpt-5-pro',
111
- name: 'GPT-5 Pro',
112
- provider: 'openai_responses',
113
- family: 'gpt-5',
114
- context_window: 400_000,
115
- max_output_tokens: 128_000,
116
- modalities: { input: %w[text image], output: ['text'] },
117
- capabilities: %w[streaming function_calling structured_output vision reasoning web_search code_interpreter]
118
- },
119
- {
120
- id: 'gpt-5-mini',
121
- name: 'GPT-5 Mini',
122
- provider: 'openai_responses',
123
- family: 'gpt-5',
124
- context_window: 400_000,
125
- max_output_tokens: 128_000,
126
- modalities: { input: %w[text image], output: ['text'] },
127
- capabilities: %w[streaming function_calling structured_output vision reasoning web_search code_interpreter]
128
- },
129
- {
130
- id: 'gpt-5-nano',
131
- name: 'GPT-5 Nano',
132
- provider: 'openai_responses',
133
- family: 'gpt-5',
134
- context_window: 400_000,
135
- max_output_tokens: 128_000,
136
- modalities: { input: %w[text image], output: ['text'] },
137
- capabilities: %w[streaming function_calling structured_output vision reasoning]
138
- },
139
-
140
- # ===================
141
- # O-Series Reasoning Models
142
- # ===================
143
- {
144
- id: 'o4-mini',
145
- name: 'O4 Mini',
146
- provider: 'openai_responses',
147
- family: 'o4',
148
- context_window: 200_000,
149
- max_output_tokens: 100_000,
150
- modalities: { input: %w[text image], output: ['text'] },
151
- capabilities: %w[streaming function_calling structured_output vision reasoning web_search code_interpreter]
152
- },
153
- {
154
- id: 'o3-pro',
155
- name: 'O3 Pro',
156
- provider: 'openai_responses',
157
- family: 'o3',
158
- context_window: 200_000,
159
- max_output_tokens: 100_000,
160
- modalities: { input: %w[text image], output: ['text'] },
161
- capabilities: %w[streaming function_calling structured_output vision reasoning web_search code_interpreter]
162
- },
163
- {
164
- id: 'o3',
165
- name: 'O3',
166
- provider: 'openai_responses',
167
- family: 'o3',
168
- context_window: 200_000,
169
- max_output_tokens: 100_000,
170
- modalities: { input: %w[text image], output: ['text'] },
171
- capabilities: %w[streaming function_calling structured_output vision reasoning web_search code_interpreter]
172
- },
173
- {
174
- id: 'o3-mini',
175
- name: 'O3 Mini',
176
- provider: 'openai_responses',
177
- family: 'o3',
178
- context_window: 200_000,
179
- max_output_tokens: 100_000,
180
- modalities: { input: ['text'], output: ['text'] },
181
- capabilities: %w[streaming function_calling structured_output reasoning]
182
- },
183
- {
184
- id: 'o1',
185
- name: 'O1',
186
- provider: 'openai_responses',
187
- family: 'o1',
188
- context_window: 200_000,
189
- max_output_tokens: 100_000,
190
- modalities: { input: %w[text image], output: ['text'] },
191
- capabilities: %w[streaming function_calling structured_output vision reasoning]
192
- },
193
- {
194
- id: 'o1-mini',
195
- name: 'O1 Mini',
196
- provider: 'openai_responses',
197
- family: 'o1',
198
- context_window: 128_000,
199
- max_output_tokens: 65_536,
200
- modalities: { input: ['text'], output: ['text'] },
201
- capabilities: %w[streaming function_calling structured_output reasoning]
202
- },
203
-
204
- # ===================
205
- # GPT-4.1 Series (Legacy - still supported)
206
- # ===================
207
- {
208
- id: 'gpt-4.1',
209
- name: 'GPT-4.1',
210
- provider: 'openai_responses',
211
- family: 'gpt-4.1',
212
- context_window: 1_000_000,
213
- max_output_tokens: 32_768,
214
- modalities: { input: %w[text image], output: ['text'] },
215
- capabilities: %w[streaming function_calling structured_output vision web_search code_interpreter]
216
- },
217
- {
218
- id: 'gpt-4.1-mini',
219
- name: 'GPT-4.1 Mini',
220
- provider: 'openai_responses',
221
- family: 'gpt-4.1',
222
- context_window: 1_000_000,
223
- max_output_tokens: 32_768,
224
- modalities: { input: %w[text image], output: ['text'] },
225
- capabilities: %w[streaming function_calling structured_output vision web_search code_interpreter]
226
- },
227
- {
228
- id: 'gpt-4.1-nano',
229
- name: 'GPT-4.1 Nano',
230
- provider: 'openai_responses',
231
- family: 'gpt-4.1',
232
- context_window: 1_000_000,
233
- max_output_tokens: 32_768,
234
- modalities: { input: %w[text image], output: ['text'] },
235
- capabilities: %w[streaming function_calling structured_output vision web_search code_interpreter]
236
- },
237
-
238
- # ===================
239
- # GPT-4o Series (Legacy - still widely used)
240
- # ===================
241
- {
242
- id: 'gpt-4o',
243
- name: 'GPT-4o',
244
- provider: 'openai_responses',
245
- family: 'gpt-4o',
246
- context_window: 128_000,
247
- max_output_tokens: 16_384,
248
- modalities: { input: %w[text image], output: ['text'] },
249
- capabilities: %w[streaming function_calling structured_output vision web_search code_interpreter]
250
- },
251
- {
252
- id: 'gpt-4o-mini',
253
- name: 'GPT-4o Mini',
254
- provider: 'openai_responses',
255
- family: 'gpt-4o',
256
- context_window: 128_000,
257
- max_output_tokens: 16_384,
258
- modalities: { input: %w[text image], output: ['text'] },
259
- capabilities: %w[streaming function_calling structured_output vision web_search code_interpreter]
260
- }
261
- ].freeze
12
+ native = super(:openai).all_including_unlisted.map do |source|
13
+ RubyLLM::Model.new(source.to_h.merge(provider: 'openai_responses'))
14
+ end
15
+ self.class.new((own.all_including_unlisted + native).uniq(&:id))
16
+ end
262
17
 
263
- module_function
18
+ def find(model_id, provider: nil, config: nil)
19
+ super
20
+ rescue RubyLLM::ModelNotFoundError
21
+ raise unless provider.to_s == 'openai_responses'
264
22
 
265
- def register_all!
266
- MODELS.each do |model_data|
267
- model = RubyLLM::Model::Info.new(model_data)
268
- existing = RubyLLM::Models.instance.all.find { |m| m.id == model.id && m.provider == model.provider }
269
- RubyLLM::Models.instance.all << model unless existing
270
- end
23
+ source = super(model_id, provider: :openai, config: config)
24
+ RubyLLM::Model.new(source.to_h.merge(provider: 'openai_responses'))
271
25
  end
272
26
  end
273
27
  end
274
28
  end
275
29
  end
30
+
31
+ RubyLLM::Models.prepend(RubyLLM::Providers::OpenAIResponses::ModelRegistry)