Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
18025f9
Introduce support of Responses API
redox Jul 23, 2025
0a1c980
useless
redox Jul 23, 2025
44ab739
Merge branch 'main' into responses-api
tpaulshippy Aug 5, 2025
ab2ac42
Start simplifying by moving responses into openai provider only
tpaulshippy Aug 5, 2025
79649c6
Introduce new module to hold chat completions API stuff
tpaulshippy Aug 5, 2025
fc3945b
Add support for attaching media
tpaulshippy Aug 5, 2025
99e4d9e
Refactor a bit to support audio inputs with fallback
tpaulshippy Aug 5, 2025
a693991
Restore use of complete
tpaulshippy Aug 6, 2025
d8ff718
Setup response schema for responses API
tpaulshippy Aug 6, 2025
447100a
Update with params spec
tpaulshippy Aug 6, 2025
f8375f5
Update cassettes for chat with_schema
tpaulshippy Aug 6, 2025
f48ba3f
Remove some extra params
tpaulshippy Aug 6, 2025
c0ae2aa
OpenAI responses API does not seem to provide token counts on chunks …
tpaulshippy Aug 6, 2025
d00cc38
Update error handling with responses
tpaulshippy Aug 6, 2025
928b1c1
Handle chunks from responses when streaming
tpaulshippy Aug 6, 2025
049c8ea
Rubocop fixes
tpaulshippy Aug 6, 2025
eb53d58
Update spec for responses
tpaulshippy Aug 6, 2025
4f68ebd
One more rubocop
tpaulshippy Aug 6, 2025
f50cb23
Clean up some methods we don't need to rename or don't need
tpaulshippy Aug 6, 2025
bf2c549
Remove extra namespaces
tpaulshippy Aug 6, 2025
40e17be
Merge branch 'main' into responses-api
tpaulshippy Aug 7, 2025
b0dace8
Update some cassettes
tpaulshippy Aug 7, 2025
367f341
Rubocop -A
tpaulshippy Aug 7, 2025
f25ffdd
Merge branch 'main' into responses-api
tpaulshippy Aug 8, 2025
ea03496
Merge main into responses-api
tpaulshippy Aug 13, 2025
371e3d2
Update some cassettes
tpaulshippy Aug 14, 2025
31fd54e
Merge branch 'main' into responses-api
tpaulshippy Aug 25, 2025
485a28b
Update cassettes
tpaulshippy Aug 25, 2025
02e1d5c
Unable to generate this cassette for some reason, just restore what w…
tpaulshippy Aug 25, 2025
9133c7c
Cleanup some comments and deprecated stuff
tpaulshippy Aug 27, 2025
bf4b381
Merge branch 'main' into responses-api
tpaulshippy Aug 27, 2025
6cda94f
Fix model id in responses API payload
tpaulshippy Aug 28, 2025
1a97543
Merge branch 'main' into responses-api
tpaulshippy Oct 17, 2025
27213bc
Fix some broken specs after merge
tpaulshippy Oct 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions gemfiles/rails_7.1.gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ GEM
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (~> 0.7)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
fiber-annotation (0.2.0)
fiber-local (1.1.0)
Expand Down Expand Up @@ -180,7 +181,7 @@ GEM
ruby-vips (>= 2.0.17, < 3)
iniparse (1.5.0)
io-console (0.8.1)
io-event (1.14.0)
io-event (1.11.2)
irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
Expand Down Expand Up @@ -224,6 +225,8 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.10-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-gnu)
racc (~> 1.4)
os (1.1.4)
Expand All @@ -239,7 +242,7 @@ GEM
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
prism (1.5.2)
prism (1.6.0)
pry (0.15.2)
coderay (~> 1.1)
method_source (~> 1.0)
Expand Down Expand Up @@ -356,6 +359,7 @@ GEM
simplecov (~> 0.19)
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
sqlite3 (2.7.4-arm64-darwin)
sqlite3 (2.7.4-x86_64-linux-gnu)
stringio (3.1.7)
thor (1.4.0)
Expand All @@ -382,6 +386,7 @@ GEM
zeitwerk (2.7.3)

PLATFORMS
arm64-darwin-22
x86_64-linux

DEPENDENCIES
Expand Down
9 changes: 7 additions & 2 deletions gemfiles/rails_7.2.gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ GEM
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (~> 0.7)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
fiber-annotation (0.2.0)
fiber-local (1.1.0)
Expand Down Expand Up @@ -174,7 +175,7 @@ GEM
ruby-vips (>= 2.0.17, < 3)
iniparse (1.5.0)
io-console (0.8.1)
io-event (1.14.0)
io-event (1.11.2)
irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
Expand Down Expand Up @@ -217,6 +218,8 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.10-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-gnu)
racc (~> 1.4)
os (1.1.4)
Expand All @@ -232,7 +235,7 @@ GEM
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
prism (1.5.2)
prism (1.6.0)
pry (0.15.2)
coderay (~> 1.1)
method_source (~> 1.0)
Expand Down Expand Up @@ -349,6 +352,7 @@ GEM
simplecov (~> 0.19)
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
sqlite3 (2.7.4-arm64-darwin)
sqlite3 (2.7.4-x86_64-linux-gnu)
stringio (3.1.7)
thor (1.4.0)
Expand Down Expand Up @@ -376,6 +380,7 @@ GEM
zeitwerk (2.7.3)

PLATFORMS
arm64-darwin-22
x86_64-linux

DEPENDENCIES
Expand Down
9 changes: 7 additions & 2 deletions gemfiles/rails_8.0.gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ GEM
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (~> 0.7)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
fiber-annotation (0.2.0)
fiber-local (1.1.0)
Expand Down Expand Up @@ -174,7 +175,7 @@ GEM
ruby-vips (>= 2.0.17, < 3)
iniparse (1.5.0)
io-console (0.8.1)
io-event (1.14.0)
io-event (1.11.2)
irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
Expand Down Expand Up @@ -217,6 +218,8 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.10-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-gnu)
racc (~> 1.4)
os (1.1.4)
Expand All @@ -232,7 +235,7 @@ GEM
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
prism (1.5.2)
prism (1.6.0)
pry (0.15.2)
coderay (~> 1.1)
method_source (~> 1.0)
Expand Down Expand Up @@ -350,6 +353,7 @@ GEM
simplecov (~> 0.19)
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
sqlite3 (2.7.4-arm64-darwin)
sqlite3 (2.7.4-x86_64-linux-gnu)
stringio (3.1.7)
thor (1.4.0)
Expand Down Expand Up @@ -377,6 +381,7 @@ GEM
zeitwerk (2.7.3)

PLATFORMS
arm64-darwin-22
x86_64-linux

DEPENDENCIES
Expand Down
1 change: 1 addition & 0 deletions lib/ruby_llm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
'ruby_llm' => 'RubyLLM',
'llm' => 'LLM',
'openai' => 'OpenAI',
'openai_base' => 'OpenAIBase',
'api' => 'API',
'deepseek' => 'DeepSeek',
'perplexity' => 'Perplexity',
Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_llm/providers/deepseek.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module RubyLLM
module Providers
# DeepSeek API integration.
class DeepSeek < OpenAI
class DeepSeek < OpenAIBase
include DeepSeek::Chat

def api_base
Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_llm/providers/gpustack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module RubyLLM
module Providers
# GPUStack API integration based on Ollama.
class GPUStack < OpenAI
class GPUStack < OpenAIBase
include GPUStack::Chat
include GPUStack::Models
include GPUStack::Media
Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_llm/providers/mistral.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module RubyLLM
module Providers
# Mistral API integration.
class Mistral < OpenAI
class Mistral < OpenAIBase
include Mistral::Chat
include Mistral::Models
include Mistral::Embeddings
Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_llm/providers/ollama.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module RubyLLM
module Providers
# Ollama API integration.
class Ollama < OpenAI
class Ollama < OpenAIBase
include Ollama::Chat
include Ollama::Media
include Ollama::Models
Expand Down
51 changes: 25 additions & 26 deletions lib/ruby_llm/providers/openai.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,38 @@
module RubyLLM
module Providers
# OpenAI API integration.
class OpenAI < Provider
include OpenAI::Chat
include OpenAI::Embeddings
include OpenAI::Models
include OpenAI::Moderation
include OpenAI::Streaming
include OpenAI::Tools
include OpenAI::Images
include OpenAI::Media
class OpenAI < OpenAIBase
include OpenAI::Response
include OpenAI::ResponseMedia

def api_base
@config.openai_api_base || 'https://api.openai.com/v1'
end
def audio_input?(messages)
messages.any? do |message|
next false unless message.respond_to?(:content) && message.content.respond_to?(:attachments)

def headers
{
'Authorization' => "Bearer #{@config.openai_api_key}",
'OpenAI-Organization' => @config.openai_organization_id,
'OpenAI-Project' => @config.openai_project_id
}.compact
message.content.attachments.any? { |attachment| attachment.type == :audio }
end
end

def maybe_normalize_temperature(temperature, model)
OpenAI::Capabilities.normalize_temperature(temperature, model.id)
end
def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Metrics/ParameterLists
@using_responses_api = !audio_input?(messages)

class << self
def capabilities
OpenAI::Capabilities
if @using_responses_api
render_response_payload(messages, tools: tools, temperature: temperature, model: model, stream: stream,
schema: schema)
else
super
end
end

def completion_url
@using_responses_api ? responses_url : super
end

def configuration_requirements
%i[openai_api_key]
def parse_completion_response(response)
if @using_responses_api
parse_respond_response(response)
else
super
end
end
end
Expand Down
3 changes: 2 additions & 1 deletion lib/ruby_llm/providers/openai/chat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ def render_payload(messages, tools:, temperature:, model:, stream: false, schema
}

payload[:temperature] = temperature unless temperature.nil?
payload[:tools] = tools.map { |_, tool| tool_for(tool) } if tools.any?

payload[:tools] = tools.map { |_, tool| chat_tool_for(tool) } if tools.any?

if schema
strict = schema[:strict] != false
Expand Down
115 changes: 115 additions & 0 deletions lib/ruby_llm/providers/openai/response.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# frozen_string_literal: true

module RubyLLM
module Providers
class OpenAI
# Response methods of the OpenAI API integration
module Response
def responses_url
'responses'
end

module_function

def render_response_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Metrics/ParameterLists
payload = {
model: model.id,
input: format_input(messages),
stream: stream
}

# Only include temperature if it's not nil (some models don't accept it)
payload[:temperature] = temperature unless temperature.nil?

payload[:tools] = tools.map { |_, tool| response_tool_for(tool) } if tools.any?

if schema
# Use strict mode from schema if specified, default to true
strict = schema[:strict] != false

payload[:text] = {
format: {
type: 'json_schema',
name: 'response',
schema: schema,
strict: strict
}
}
end

payload
end

def format_input(messages) # rubocop:disable Metrics/PerceivedComplexity
all_tool_calls = messages.flat_map do |m|
m.tool_calls&.values || []
end
messages.flat_map do |msg|
if msg.tool_call?
msg.tool_calls.map do |_, tc|
{
type: 'function_call',
call_id: tc.id,
name: tc.name,
arguments: JSON.generate(tc.arguments),
status: 'completed'
}
end
elsif msg.role == :tool
{
type: 'function_call_output',
call_id: all_tool_calls.detect { |tc| tc.id == msg.tool_call_id }&.id,
output: msg.content,
status: 'completed'
}
else
{
type: 'message',
role: format_role(msg.role),
content: ResponseMedia.format_content(msg.content),
status: 'completed'
}.compact
end
end
end

def format_role(role)
case role
when :system
'developer'
else
role.to_s
end
end

def parse_respond_response(response)
data = response.body
return if data.empty?

raise Error.new(response, data.dig('error', 'message')) if data.dig('error', 'message')

outputs = data['output']
return unless outputs.any?

Message.new(
role: :assistant,
content: all_output_text(outputs),
tool_calls: parse_response_tool_calls(outputs),
input_tokens: data['usage']['input_tokens'],
output_tokens: data['usage']['output_tokens'],
model_id: data['model'],
raw: response
)
end

def all_output_text(outputs)
outputs.select { |o| o['type'] == 'message' }.flat_map do |o|
o['content'].filter_map do |c|
c['type'] == 'output_text' && c['text']
end
end.join("\n")
end
end
end
end
end
Loading