Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.1.0-beta.2"
".": "0.4.0-beta.1"
}
2 changes: 1 addition & 1 deletion .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 109
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-fc64d7c2c8f51f750813375356c3f3fdfc7fc1b1b34f19c20a5410279d445d37.yml
openapi_spec_hash: 618285fc70199ee32b9ebe4bf72f7e4c
config_hash: c497f6b750cc89c0bf2eefc0bc839c70
config_hash: 535b6e5f26a295d609b259c8cb8f656c
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## 0.4.0-beta.1 (2025-05-23)

Full Changelog: [v0.1.0-beta.2...v0.4.0-beta.1](https://github.com/openai/openai-ruby/compare/v0.1.0-beta.2...v0.4.0-beta.1)

### Features

* structured output for responses API (text) ([#688](https://github.com/openai/openai-ruby/issues/688)) ([282ec24](https://github.com/openai/openai-ruby/commit/282ec24c89511c1cd50029fe154e10d772e23239))
* structured output for responses API (tools) ([#691](https://github.com/openai/openai-ruby/issues/691)) ([5e524ea](https://github.com/openai/openai-ruby/commit/5e524ea48020125911204c8050b49e360d7513d7))


### Chores

* **internal:** fix release workflows ([e1b31a6](https://github.com/openai/openai-ruby/commit/e1b31a6d6c3064f57e82aa1c3f48f2c797619b5a))
* **internal:** version bump ([b2dd8dd](https://github.com/openai/openai-ruby/commit/b2dd8dd1aac3ff9acf69953a0d04c74721b47e36))
* update README for public release ([#145](https://github.com/openai/openai-ruby/issues/145)) ([64e3849](https://github.com/openai/openai-ruby/commit/64e384933c2f80508002dfacf89efe74510c1330))

## 0.1.0-beta.2 (2025-05-22)

Full Changelog: [v0.1.0-beta.1...v0.1.0-beta.2](https://github.com/openai/openai-ruby/compare/v0.1.0-beta.1...v0.1.0-beta.2)
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ GIT
PATH
remote: .
specs:
openai (0.1.0.pre.beta.2)
openai (0.4.0.pre.beta.1)
connection_pool

GEM
Expand Down
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ The REST API documentation can be found on [platform.openai.com](https://platfor

## Installation

ℹ️ The `openai` gem is not yet available on [rubygems.org](https://rubygems.org).

To use this gem, install via Bundler by adding the following to your application's `Gemfile`:

<!-- x-release-please-start-version -->

```ruby
gem "openai", github: "openai/openai-ruby", branch: "main"
gem "openai", "~> 0.4.0.pre.beta.1"
```

<!-- x-release-please-end -->
Expand Down
4 changes: 4 additions & 0 deletions bin/check-release-environment
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

errors=()

if [ -z "${STAINLESS_API_KEY}" ]; then
errors+=("The STAINLESS_API_KEY secret has not been set. Please contact Stainless for an API key & set it in your organization secrets on GitHub.")
fi

if [ -z "${GEM_HOST_API_KEY}" ]; then
errors+=("The OPENAI_GEM_HOST_API_KEY secret has not been set. Please set it in either this repository's secrets or your organization secrets")
fi
Expand Down
2 changes: 1 addition & 1 deletion examples/structured_outputs_chat_completions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class CalendarEvent < OpenAI::BaseModel

chat_completion
.choices
.filter { !_1.message.refusal }
.reject { _1.message.refusal }
.each do |choice|
pp(choice.message.parsed)
end
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class GetWeather < OpenAI::BaseModel

chat_completion
.choices
.filter { !_1.message.refusal }
.reject { _1.message.refusal }
.flat_map { _1.message.tool_calls.to_a }
.each do |tool_call|
pp(tool_call.function.parsed)
Expand Down
55 changes: 55 additions & 0 deletions examples/structured_outputs_responses.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require_relative "../lib/openai"

class Location < OpenAI::BaseModel
required :address, String
required :city, String, doc: "City name"
required :postal_code, String, nil?: true
end

# Participant model with an optional last_name and an enum for status
class Participant < OpenAI::BaseModel
required :first_name, String
required :last_name, String, nil?: true
required :status, OpenAI::EnumOf[:confirmed, :unconfirmed, :tentative]
end

# CalendarEvent model with a list of participants.
class CalendarEvent < OpenAI::BaseModel
required :name, String
required :date, String
required :participants, OpenAI::ArrayOf[Participant]
required :is_virtual, OpenAI::Boolean
required :location,
OpenAI::UnionOf[String, Location],
nil?: true,
doc: "Event location"
end

client = OpenAI::Client.new

response = client.responses.create(
model: "gpt-4o-2024-08-06",
input: [
{role: :system, content: "Extract the event information."},
{
role: :user,
content: <<~CONTENT
Alice Shah and Lena are going to a science fair on Friday at 123 Main St. in San Diego.
They have also invited Jasper Vellani and Talia Groves - Jasper has not responded and Talia said she is thinking about it.
CONTENT
}
],
text: CalendarEvent
)

response
.output
.flat_map { _1.content }
# filter out refusal responses
.grep_v(OpenAI::Models::Responses::ResponseOutputRefusal)
.each do |content|
pp(content.parsed)
end
28 changes: 28 additions & 0 deletions examples/structured_outputs_responses_function_calling.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require_relative "../lib/openai"

class GetWeather < OpenAI::BaseModel
required :location, String, doc: "City and country e.g. Bogotá, Colombia"
end

# gets API Key from environment variable `OPENAI_API_KEY`
client = OpenAI::Client.new

response = client.responses.create(
model: "gpt-4o-2024-08-06",
input: [
{
role: :user,
content: "What's the weather like in Paris today?"
}
],
tools: [GetWeather]
)

response
.output
.each do |output|
pp(output.parsed)
end
2 changes: 1 addition & 1 deletion lib/openai/models/chat/completion_create_params.rb
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ module ResponseFormat
# Learn more about [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs).
variant -> { OpenAI::ResponseFormatJSONSchema }

# An {OpenAI::BaseModel} can be provided and implicitly converted into {OpenAI::ResponseFormatJSONSchema}.
# An {OpenAI::BaseModel} can be provided and implicitly converted into {OpenAI::Models::ResponseFormatJSONSchema}.
# See examples for more details.
#
# Learn more about [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs).
Expand Down
2 changes: 1 addition & 1 deletion lib/openai/models/response_format_json_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class JSONSchema < OpenAI::Internal::Type::BaseModel
# @return [Hash{Symbol=>Object}, nil]
optional :schema,
union: -> {
OpenAI::StructuredOutput::UnionOf[
OpenAI::UnionOf[
OpenAI::Internal::Type::HashOf[OpenAI::Internal::Type::Unknown],
OpenAI::StructuredOutput::JsonSchemaConverter
]
Expand Down
7 changes: 6 additions & 1 deletion lib/openai/models/responses/function_tool.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ class FunctionTool < OpenAI::Internal::Type::BaseModel
# A JSON schema object describing the parameters of the function.
#
# @return [Hash{Symbol=>Object}, nil]
required :parameters, OpenAI::Internal::Type::HashOf[OpenAI::Internal::Type::Unknown], nil?: true
required :parameters,
union: OpenAI::UnionOf[
OpenAI::Internal::Type::HashOf[OpenAI::Internal::Type::Unknown],
OpenAI::StructuredOutput::JsonSchemaConverter
],
nil?: true

# @!attribute strict
# Whether to enforce strict parameter validation. Default `true`.
Expand Down
12 changes: 9 additions & 3 deletions lib/openai/models/responses/response_create_params.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,13 @@ class ResponseCreateParams < OpenAI::Internal::Type::BaseModel
# - [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs)
#
# @return [OpenAI::Models::Responses::ResponseTextConfig, nil]
optional :text, -> { OpenAI::Responses::ResponseTextConfig }
optional :text,
union: -> {
OpenAI::UnionOf[
OpenAI::Responses::ResponseTextConfig,
OpenAI::StructuredOutput::JsonSchemaConverter
]
}

# @!attribute tool_choice
# How the model should select which tool (or tools) to use when generating a
Expand All @@ -185,7 +191,7 @@ class ResponseCreateParams < OpenAI::Internal::Type::BaseModel
# the model to call your own code. Learn more about
# [function calling](https://platform.openai.com/docs/guides/function-calling).
#
# @return [Array<OpenAI::Models::Responses::FunctionTool, OpenAI::Models::Responses::FileSearchTool, OpenAI::Models::Responses::ComputerTool, OpenAI::Models::Responses::Tool::Mcp, OpenAI::Models::Responses::Tool::CodeInterpreter, OpenAI::Models::Responses::Tool::ImageGeneration, OpenAI::Models::Responses::Tool::LocalShell, OpenAI::Models::Responses::WebSearchTool>, nil]
# @return [Array<OpenAI::Models::Responses::FunctionTool, OpenAI::StructuredOutput::JsonSchemaConverter, OpenAI::Models::Responses::FileSearchTool, OpenAI::Models::Responses::ComputerTool, OpenAI::Models::Responses::Tool::Mcp, OpenAI::Models::Responses::Tool::CodeInterpreter, OpenAI::Models::Responses::Tool::ImageGeneration, OpenAI::Models::Responses::Tool::LocalShell, OpenAI::Models::Responses::WebSearchTool>, nil]
optional :tools, -> { OpenAI::Internal::Type::ArrayOf[union: OpenAI::Responses::Tool] }

# @!attribute top_p
Expand Down Expand Up @@ -252,7 +258,7 @@ class ResponseCreateParams < OpenAI::Internal::Type::BaseModel
#
# @param tool_choice [Symbol, OpenAI::Models::Responses::ToolChoiceOptions, OpenAI::Models::Responses::ToolChoiceTypes, OpenAI::Models::Responses::ToolChoiceFunction] How the model should select which tool (or tools) to use when generating
#
# @param tools [Array<OpenAI::Models::Responses::FunctionTool, OpenAI::Models::Responses::FileSearchTool, OpenAI::Models::Responses::ComputerTool, OpenAI::Models::Responses::Tool::Mcp, OpenAI::Models::Responses::Tool::CodeInterpreter, OpenAI::Models::Responses::Tool::ImageGeneration, OpenAI::Models::Responses::Tool::LocalShell, OpenAI::Models::Responses::WebSearchTool>] An array of tools the model may call while generating a response. You
# @param tools [Array<OpenAI::Models::Responses::FunctionTool, OpenAI::StructuredOutput::JsonSchemaConverter, OpenAI::Models::Responses::FileSearchTool, OpenAI::Models::Responses::ComputerTool, OpenAI::Models::Responses::Tool::Mcp, OpenAI::Models::Responses::Tool::CodeInterpreter, OpenAI::Models::Responses::Tool::ImageGeneration, OpenAI::Models::Responses::Tool::LocalShell, OpenAI::Models::Responses::WebSearchTool>] An array of tools the model may call while generating a response. You
#
# @param top_p [Float, nil] An alternative to sampling with temperature, called nucleus sampling,
#
Expand Down
6 changes: 6 additions & 0 deletions lib/openai/models/responses/response_format_text_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ module ResponseFormatTextConfig
# Learn more about [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs).
variant :json_schema, -> { OpenAI::Responses::ResponseFormatTextJSONSchemaConfig }

# An {OpenAI::BaseModel} can be provided and implicitly converted into {OpenAI::Models::Responses::ResponseFormatTextJSONSchemaConfig}.
# See examples for more details.
#
# Learn more about [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs).
variant -> { OpenAI::StructuredOutput::JsonSchemaConverter }

# JSON object response format. An older method of generating JSON responses.
# Using `json_schema` is recommended for models that support it. Note that the
# model will not generate JSON without a system or user message instructing it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ class ResponseFormatTextJSONSchemaConfig < OpenAI::Internal::Type::BaseModel
# The schema for the response format, described as a JSON Schema object. Learn how
# to build JSON schemas [here](https://json-schema.org/).
#
# @return [Hash{Symbol=>Object}]
required :schema, OpenAI::Internal::Type::HashOf[OpenAI::Internal::Type::Unknown]
# @return [Hash{Symbol=>Object}, OpenAI::StructuredOutput::JsonSchemaConverter]
required :schema,
union: -> {
OpenAI::UnionOf[
OpenAI::Internal::Type::HashOf[OpenAI::Internal::Type::Unknown], OpenAI::StructuredOutput::JsonSchemaConverter
]
}

# @!attribute type
# The type of response format being defined. Always `json_schema`.
Expand Down Expand Up @@ -52,7 +57,7 @@ class ResponseFormatTextJSONSchemaConfig < OpenAI::Internal::Type::BaseModel
#
# @param name [String] The name of the response format. Must be a-z, A-Z, 0-9, or contain
#
# @param schema [Hash{Symbol=>Object}] The schema for the response format, described as a JSON Schema object.
# @param schema [Hash{Symbol=>Object}, OpenAI::StructuredOutput::JsonSchemaConverter] The schema for the response format, described as a JSON Schema object.
#
# @param description [String] A description of what the response format is for, used by the model to
#
Expand Down
6 changes: 6 additions & 0 deletions lib/openai/models/responses/response_function_tool_call.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ class ResponseFunctionToolCall < OpenAI::Internal::Type::BaseModel
# @return [String]
required :arguments, String

# @!attribute parsed
# The parsed contents of the arguments.
#
# @return [Object, nil]
required :parsed, OpenAI::Internal::Type::Unknown

# @!attribute call_id
# The unique ID of the function tool call generated by the model.
#
Expand Down
6 changes: 6 additions & 0 deletions lib/openai/models/responses/response_output_text.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ class ResponseOutputText < OpenAI::Internal::Type::BaseModel
# @return [String]
required :text, String

# @!attribute parsed
# The parsed contents of the output, if JSON schema is specified.
#
# @return [Object, nil]
optional :parsed, OpenAI::Internal::Type::Unknown

# @!attribute type
# The type of the output text. Always `output_text`.
#
Expand Down
2 changes: 2 additions & 0 deletions lib/openai/models/responses/tool.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ module Tool
# Defines a function in your own code the model can choose to call. Learn more about [function calling](https://platform.openai.com/docs/guides/function-calling).
variant :function, -> { OpenAI::Responses::FunctionTool }

variant -> { OpenAI::StructuredOutput::JsonSchemaConverter }

# A tool that searches for relevant content from uploaded files. Learn more about the [file search tool](https://platform.openai.com/docs/guides/tools-file-search).
variant :file_search, -> { OpenAI::Responses::FileSearchTool }

Expand Down
75 changes: 75 additions & 0 deletions lib/openai/resources/responses.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,85 @@ def create(params)
message = "Please use `#stream_raw` for the streaming use case."
raise ArgumentError.new(message)
end

model = nil
tool_models = {}
case parsed
in {text: OpenAI::StructuredOutput::JsonSchemaConverter => model}
parsed.update(
text: {
format: {
type: :json_schema,
strict: true,
name: model.name.split("::").last,
schema: model.to_json_schema
}
}
)
in {text: {format: OpenAI::StructuredOutput::JsonSchemaConverter => model}}
parsed.fetch(:text).update(
format: {
type: :json_schema,
strict: true,
name: model.name.split("::").last,
schema: model.to_json_schema
}
)
in {text: {format: {type: :json_schema, schema: OpenAI::StructuredOutput::JsonSchemaConverter => model}}}
parsed.dig(:text, :format).store(:schema, model.to_json_schema)
in {tools: Array => tools}
mapped = tools.map do |tool|
case tool
in OpenAI::StructuredOutput::JsonSchemaConverter
name = tool.name.split("::").last
tool_models.store(name, tool)
{
type: :function,
strict: true,
name: name,
parameters: tool.to_json_schema
}
in {type: :function, parameters: OpenAI::StructuredOutput::JsonSchemaConverter => params}
func = tool.fetch(:function)
name = func[:name] ||= params.name.split("::").last
tool_models.store(name, params)
func.update(parameters: params.to_json_schema)
else
end
end
tools.replace(mapped)
else
end

unwrap = ->(raw) do
if model.is_a?(OpenAI::StructuredOutput::JsonSchemaConverter)
raw[:output]
&.flat_map do |output|
next [] unless output[:type] == "message"
output[:content].to_a
end
&.each do |content|
next unless content[:type] == "output_text"
parsed = JSON.parse(content.fetch(:text), symbolize_names: true)
coerced = OpenAI::Internal::Type::Converter.coerce(model, parsed)
content.store(:parsed, coerced)
end
end
raw[:output]&.each do |output|
next unless output[:type] == "function_call"
next if (model = tool_models[output.fetch(:name)]).nil?
parsed = JSON.parse(output.fetch(:arguments), symbolize_names: true)
coerced = OpenAI::Internal::Type::Converter.coerce(model, parsed)
output.store(:parsed, coerced)
end

raw
end
@client.request(
method: :post,
path: "responses",
body: parsed,
unwrap: unwrap,
model: OpenAI::Responses::Response,
options: options
)
Expand Down
Loading