Module: Smith::Providers::OpenAI::Responses
- Defined in:
- lib/smith/providers/openai/responses.rb
Overview
Responses API adapter consumed by Smith::Providers::OpenAI::Routing when Smith::Models::Normalizer flags a request for routing via OpenAI /v1/responses (typically: gpt-5 family + tools + thinking).
Constant Summary collapse
- RESPONSE_REASONING_TEXT_TYPES =
—- Vendored verbatim from PR #770 responses.rb —————–
%w[summary_text output_text].freeze
Class Method Summary collapse
- .apply_response_schema(payload, schema) ⇒ Object
- .apply_response_thinking(payload, thinking) ⇒ Object
- .apply_response_tools(payload, tools, native_tools, tool_prefs) ⇒ Object
-
.complete(provider, messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, thinking: nil, tool_prefs: nil, &block) ⇒ Object
SMITH-AUTHORED entry point.
- .format_response_attachment(attachment) ⇒ Object
- .format_response_content(content) ⇒ Object
-
.format_response_input(messages) ⇒ Object
rubocop:enable Metrics/ParameterLists.
- .format_response_message(message, provider: nil) ⇒ Object
- .format_response_text(text) ⇒ Object
- .format_response_tool_calls(tool_calls) ⇒ Object
- .format_response_tool_result(message) ⇒ Object
-
.format_role(role, provider: nil) ⇒ Object
—- SMITH-AUTHORED helpers (inlined from PR #770 chat.rb) ——-.
-
.parse_response_response(response, provider: nil) ⇒ Object
SMITH-AUTHORED kwarg addition: ‘provider:` is passed in so this standalone module can read `@config.openai_use_system_role` for `format_role`.
-
.render_response_payload(messages, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil, tool_prefs: nil, native_tools: nil) ⇒ Object
rubocop:disable Metrics/ParameterLists.
- .resolve_effort(thinking) ⇒ Object
- .response_output_text(data) ⇒ Object
- .response_output_text_parts(outputs) ⇒ Object
- .response_reasoning_text(outputs) ⇒ Object
- .response_tool_output(content) ⇒ Object
- .responses_url ⇒ Object
Class Method Details
.apply_response_schema(payload, schema) ⇒ Object
191 192 193 194 195 196 197 198 199 200 |
# File 'lib/smith/providers/openai/responses.rb', line 191 def self.apply_response_schema(payload, schema) payload[:text] = { format: { type: "json_schema", name: schema[:name], schema: schema[:schema], strict: schema[:strict] } } end |
.apply_response_thinking(payload, thinking) ⇒ Object
202 203 204 205 |
# File 'lib/smith/providers/openai/responses.rb', line 202 def self.apply_response_thinking(payload, thinking) effort = resolve_effort(thinking) payload[:reasoning] = { effort: effort } if effort end |
.apply_response_tools(payload, tools, native_tools, tool_prefs) ⇒ Object
181 182 183 184 185 186 187 188 189 |
# File 'lib/smith/providers/openai/responses.rb', line 181 def self.apply_response_tools(payload, tools, native_tools, tool_prefs) response_tools = tools.map { |_, tool| ToolsExtensions.response_tool_for(tool) } response_tools.concat(::RubyLLM::Utils.to_safe_array(native_tools)) payload[:tools] = response_tools if response_tools.any? unless tool_prefs[:choice].nil? payload[:tool_choice] = ToolsExtensions.build_response_tool_choice(tool_prefs[:choice]) end payload[:parallel_tool_calls] = tool_prefs[:calls] == :many unless tool_prefs[:calls].nil? end |
.complete(provider, messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, thinking: nil, tool_prefs: nil, &block) ⇒ Object
SMITH-AUTHORED entry point. Routing prepend calls this with the OpenAI provider instance + the same kwargs ‘complete` would receive. Renders the /v1/responses payload using the vendored helpers, POSTs via the provider’s Faraday connection, parses the response back into a RubyLLM::Message.
Streaming is intentionally NOT supported in this initial vendor because Smith’s workflow execution path doesn’t use it. Block-given calls raise NotImplementedError with a clear message so the host can either disable streaming or fall back to ‘openai_api_mode = :off`.
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/smith/providers/openai/responses.rb', line 59 def self.complete(provider, , tools:, temperature:, model:, params: {}, headers: {}, schema: nil, thinking: nil, tool_prefs: nil, &block) if block raise NotImplementedError, "Smith::Providers::OpenAI::Responses does not yet support streaming. " \ "Streaming over /v1/responses needs a separate stream_response port from PR #770. " \ "Workaround: pass no block (sync only), or set Smith.config.openai_api_mode = :off " \ "to route via chat-completions with graceful tool-dropping." end payload = render_response_payload( , tools: tools, temperature: temperature, model: model, stream: false, schema: schema, thinking: thinking, tool_prefs: tool_prefs ) payload = ::RubyLLM::Utils.deep_merge(payload, params) unless params.empty? connection = provider.instance_variable_get(:@connection) provider_headers = provider.send(:headers) merged_headers = provider_headers.merge(headers) http_response = connection.post(responses_url, payload) do |req| merged_headers.each { |k, v| req.headers[k] = v } end parse_response_response(http_response, provider: provider) end |
.format_response_attachment(attachment) ⇒ Object
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 |
# File 'lib/smith/providers/openai/responses.rb', line 222 def self.() case .type when :image { type: "input_image", image_url: .url? ? .source.to_s : .for_llm } when :pdf { type: "input_file", filename: .filename, file_data: .for_llm } when :text format_response_text(.for_llm) when :audio raise ::RubyLLM::UnsupportedAttachmentError, "OpenAI Responses API does not support audio inputs yet" else raise ::RubyLLM::UnsupportedAttachmentError, .type end end |
.format_response_content(content) ⇒ Object
207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/smith/providers/openai/responses.rb', line 207 def self.format_response_content(content) return content.value if content.is_a?(::RubyLLM::Content::Raw) return content.to_json if content.is_a?(Hash) || content.is_a?(Array) return content unless content.is_a?(::RubyLLM::Content) parts = [] parts << format_response_text(content.text) if content.text content..each do || parts << () end parts end |
.format_response_input(messages) ⇒ Object
rubocop:enable Metrics/ParameterLists
111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/smith/providers/openai/responses.rb', line 111 def self.format_response_input() .flat_map do || if .tool_call? format_response_tool_calls(.tool_calls) elsif .role == :tool format_response_tool_result() else () end end end |
.format_response_message(message, provider: nil) ⇒ Object
154 155 156 157 158 159 160 |
# File 'lib/smith/providers/openai/responses.rb', line 154 def self.(, provider: nil) { type: "message", role: format_role(.role, provider: provider), content: format_response_content(.content) }.compact end |
.format_response_text(text) ⇒ Object
244 245 246 247 248 249 |
# File 'lib/smith/providers/openai/responses.rb', line 244 def self.format_response_text(text) { type: "input_text", text: text } end |
.format_response_tool_calls(tool_calls) ⇒ Object
162 163 164 165 166 167 168 169 170 171 |
# File 'lib/smith/providers/openai/responses.rb', line 162 def self.format_response_tool_calls(tool_calls) tool_calls.map do |_, tool_call| { type: "function_call", call_id: tool_call.id, name: tool_call.name, arguments: JSON.generate(tool_call.arguments || {}) } end end |
.format_response_tool_result(message) ⇒ Object
173 174 175 176 177 178 179 |
# File 'lib/smith/providers/openai/responses.rb', line 173 def self.format_response_tool_result() { type: "function_call_output", call_id: .tool_call_id, output: response_tool_output(.content) } end |
.format_role(role, provider: nil) ⇒ Object
—- SMITH-AUTHORED helpers (inlined from PR #770 chat.rb) ——-
Upstream PR #770 keeps these on the chat module (which is mixed into the provider class so they’re available as instance methods with access to @config). Smith’s vendored Responses module is standalone (it can’t read @config), so these helpers are inlined as class methods with the provider passed in where
297 298 299 300 301 302 303 304 305 |
# File 'lib/smith/providers/openai/responses.rb', line 297 def self.format_role(role, provider: nil) case role when :system config = provider&.instance_variable_get(:@config) (config && config.respond_to?(:openai_use_system_role) && config.openai_use_system_role) ? "system" : "developer" else role.to_s end end |
.parse_response_response(response, provider: nil) ⇒ Object
SMITH-AUTHORED kwarg addition: ‘provider:` is passed in so this standalone module can read `@config.openai_use_system_role` for `format_role`. Upstream method lives on the provider instance and reads `@config` directly; Smith’s standalone module needs the indirection.
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/smith/providers/openai/responses.rb', line 128 def self.parse_response_response(response, provider: nil) # rubocop:disable Lint/UnusedMethodArgument data = response.body return if data.empty? raise ::RubyLLM::Error.new(response, data.dig("error", "message")) if data.dig("error", "message") outputs = data["output"] || [] return if outputs.empty? usage = data["usage"] || {} ::RubyLLM::Message.new( role: :assistant, content: response_output_text(data), thinking: ::RubyLLM::Thinking.build(text: response_reasoning_text(outputs)), tool_calls: ToolsExtensions.parse_response_tool_calls(outputs), input_tokens: usage["input_tokens"], output_tokens: usage["output_tokens"], cached_tokens: usage.dig("input_tokens_details", "cached_tokens"), cache_creation_tokens: usage.dig("input_tokens_details", "cache_write_tokens") || 0, thinking_tokens: usage.dig("output_tokens_details", "reasoning_tokens"), model_id: data["model"], raw: response ) end |
.render_response_payload(messages, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil, tool_prefs: nil, native_tools: nil) ⇒ Object
rubocop:disable Metrics/ParameterLists
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/smith/providers/openai/responses.rb', line 93 def self.render_response_payload(, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil, tool_prefs: nil, native_tools: nil) tool_prefs ||= {} payload = { model: model.id, input: format_response_input(), stream: stream, store: false } payload[:temperature] = temperature unless temperature.nil? apply_response_tools(payload, tools, native_tools, tool_prefs) apply_response_schema(payload, schema) if schema apply_response_thinking(payload, thinking) payload end |
.resolve_effort(thinking) ⇒ Object
307 308 309 310 311 |
# File 'lib/smith/providers/openai/responses.rb', line 307 def self.resolve_effort(thinking) return nil unless thinking thinking.respond_to?(:effort) ? thinking.effort : thinking end |
.response_output_text(data) ⇒ Object
260 261 262 263 264 265 266 |
# File 'lib/smith/providers/openai/responses.rb', line 260 def self.response_output_text(data) output_text = data["output_text"] return output_text if output_text.is_a?(String) && !output_text.empty? text = response_output_text_parts(data["output"]).join text.empty? ? nil : text end |
.response_output_text_parts(outputs) ⇒ Object
268 269 270 271 272 273 274 |
# File 'lib/smith/providers/openai/responses.rb', line 268 def self.response_output_text_parts(outputs) ::RubyLLM::Utils.to_safe_array(outputs).select { |output| output["type"] == "message" }.flat_map do |output| ::RubyLLM::Utils.to_safe_array(output["content"]).filter_map do |content| content["text"] if content["type"] == "output_text" && content["text"].is_a?(String) end end end |
.response_reasoning_text(outputs) ⇒ Object
276 277 278 279 280 281 282 283 284 285 286 |
# File 'lib/smith/providers/openai/responses.rb', line 276 def self.response_reasoning_text(outputs) text = outputs.select { |output| output["type"] == "reasoning" }.flat_map do |output| ::RubyLLM::Utils.to_safe_array(output["summary"] || output["content"]).filter_map do |content| if RESPONSE_REASONING_TEXT_TYPES.include?(content["type"]) && content["text"].is_a?(String) content["text"] end end end.join text.empty? ? nil : text end |
.response_tool_output(content) ⇒ Object
251 252 253 254 255 256 257 258 |
# File 'lib/smith/providers/openai/responses.rb', line 251 def self.response_tool_output(content) return JSON.generate(content.value) if content.is_a?(::RubyLLM::Content::Raw) return content.text.to_s if content.is_a?(::RubyLLM::Content) && content.text return JSON.generate(content.to_h) if content.is_a?(::RubyLLM::Content) return JSON.generate(content) if content.is_a?(Hash) || content.is_a?(Array) content.to_s end |
.responses_url ⇒ Object
44 45 46 |
# File 'lib/smith/providers/openai/responses.rb', line 44 def self.responses_url "responses" end |