Skip to content

Commit 00b25bd

Browse files
committed
feat: RBI type defs for structured output (#684)
1 parent 8be52a2 commit 00b25bd

17 files changed

+275
-14
lines changed

Rakefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ directory(examples)
122122

123123
desc("Typecheck `*.rbi`")
124124
multitask("typecheck:sorbet": examples) do
125-
sh(*%w[srb typecheck])
125+
sh(*%w[srb typecheck --dir], examples)
126126
end
127127

128128
directory(tapioca) do

lib/openai.rb

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# frozen_string_literal: true
22

33
# Standard libraries.
4-
# rubocop:disable Lint/RedundantRequireStatement
54
require "English"
65
require "cgi"
76
require "date"
@@ -15,8 +14,6 @@
1514
require "stringio"
1615
require "time"
1716
require "uri"
18-
# rubocop:enable Lint/RedundantRequireStatement
19-
2017
# We already ship the preferred sorbet manifests in the package itself.
2118
# `tapioca` currently does not offer us a way to opt out of unnecessary compilation.
2219
if Object.const_defined?(:Tapioca) && caller.chain([$PROGRAM_NAME]).chain(ARGV).grep(/tapioca/)

lib/openai/helpers/structured_output/union_of.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ def to_json_schema_inner(state:)
2626
mergeable_keys = {[:anyOf] => 0, [:type] => 0}
2727
schemas = variants.to_enum.with_index.map do
2828
new_state = {**state, path: [*path, "?.#{_2}"]}
29-
OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.to_json_schema_inner(_1, state: new_state)
29+
OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.to_json_schema_inner(
30+
_1,
31+
state: new_state
32+
)
3033
end
3134

3235
schemas.each do |schema|

lib/openai/models/chat/completion_create_params.rb

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ class CompletionCreateParams < OpenAI::Internal::Type::BaseModel
205205
# ensures the message the model generates is valid JSON. Using `json_schema` is
206206
# preferred for models that support it.
207207
#
208-
# @return [OpenAI::ResponseFormatText, OpenAI::ResponseFormatJSONSchema, OpenAI::ResponseFormatJSONObject, nil]
208+
# @return [OpenAI::ResponseFormatText, OpenAI::ResponseFormatJSONSchema, OpenAI::StructuredOutput::JsonSchemaConverter, OpenAI::ResponseFormatJSONObject, nil]
209209
optional :response_format, union: -> { OpenAI::Chat::CompletionCreateParams::ResponseFormat }
210210

211211
# @!attribute seed
@@ -291,8 +291,13 @@ class CompletionCreateParams < OpenAI::Internal::Type::BaseModel
291291
# tool. Use this to provide a list of functions the model may generate JSON inputs
292292
# for. A max of 128 functions are supported.
293293
#
294-
# @return [Array<OpenAI::Chat::ChatCompletionTool>, nil]
295-
optional :tools, -> { OpenAI::Internal::Type::ArrayOf[OpenAI::Chat::ChatCompletionTool] }
294+
# @return [Array<OpenAI::Chat::ChatCompletionTool, OpenAI::StructuredOutput::JsonSchemaConverter>, nil]
295+
optional :tools,
296+
-> {
297+
OpenAI::Internal::Type::ArrayOf[OpenAI::StructuredOutput::UnionOf[
298+
OpenAI::Chat::ChatCompletionTool, OpenAI::StructuredOutput::JsonSchemaConverter
299+
]]
300+
}
296301

297302
# @!attribute top_logprobs
298303
# An integer between 0 and 20 specifying the number of most likely tokens to
@@ -366,7 +371,7 @@ class CompletionCreateParams < OpenAI::Internal::Type::BaseModel
366371
#
367372
# @param reasoning_effort [Symbol, OpenAI::ReasoningEffort, nil] **o-series models only**
368373
#
369-
# @param response_format [OpenAI::ResponseFormatText, OpenAI::ResponseFormatJSONSchema, OpenAI::ResponseFormatJSONObject] An object specifying the format that the model must output.
374+
# @param response_format [OpenAI::ResponseFormatText, OpenAI::ResponseFormatJSONSchema, OpenAI::StructuredOutput::JsonSchemaConverter, OpenAI::ResponseFormatJSONObject] An object specifying the format that the model must output.
370375
#
371376
# @param seed [Integer, nil] This feature is in Beta.
372377
#
@@ -382,7 +387,7 @@ class CompletionCreateParams < OpenAI::Internal::Type::BaseModel
382387
#
383388
# @param tool_choice [Symbol, OpenAI::Chat::ChatCompletionToolChoiceOption::Auto, OpenAI::Chat::ChatCompletionNamedToolChoice] Controls which (if any) tool is called by the model.
384389
#
385-
# @param tools [Array<OpenAI::Chat::ChatCompletionTool>] A list of tools the model may call. Currently, only functions are supported as a
390+
# @param tools [Array<OpenAI::Chat::ChatCompletionTool, OpenAI::StructuredOutput::JsonSchemaConverter>] A list of tools the model may call. Currently, only functions are supported as a
386391
#
387392
# @param top_logprobs [Integer, nil] An integer between 0 and 20 specifying the number of most likely tokens to
388393
#
@@ -525,6 +530,12 @@ module ResponseFormat
525530
# Learn more about [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs).
526531
variant -> { OpenAI::ResponseFormatJSONSchema }
527532

533+
# An {OpenAI::BaseModel} can be provided and implicitly converted into {OpenAI::ResponseFormatJSONSchema}.
534+
# See examples for more details.
535+
#
536+
# Learn more about [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs).
537+
variant -> { OpenAI::StructuredOutput::JsonSchemaConverter }
538+
528539
# JSON object response format. An older method of generating JSON responses.
529540
# Using `json_schema` is recommended for models that support it. Note that the
530541
# model will not generate JSON without a system or user message instructing it

lib/openai/structured_output.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
module OpenAI
4+
StructuredOutput = OpenAI::Helpers::StructuredOutput
45
ArrayOf = OpenAI::Helpers::StructuredOutput::ArrayOf
56
BaseModel = OpenAI::Helpers::StructuredOutput::BaseModel
67
Boolean = OpenAI::Helpers::StructuredOutput::Boolean
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# typed: strong
2+
3+
module OpenAI
4+
module Helpers
5+
# Helpers for the structured output API.
6+
#
7+
# see https://platform.openai.com/docs/guides/structured-outputs
8+
# see https://json-schema.org
9+
#
10+
# Based on the DSL in {OpenAI::Internal::Type}, but currently only support the limited subset of JSON schema types used in structured output APIs.
11+
#
12+
# Supported types: {NilClass} {String} {Symbol} {Integer} {Float} {OpenAI::Boolean}, {OpenAI::EnumOf}, {OpenAI::UnionOf}, {OpenAI::ArrayOf}, {OpenAI::BaseModel}
13+
module StructuredOutput
14+
end
15+
end
16+
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# typed: strong
2+
3+
module OpenAI
4+
module Helpers
5+
module StructuredOutput
6+
class ArrayOf < OpenAI::Internal::Type::ArrayOf
7+
include OpenAI::Helpers::StructuredOutput::JsonSchemaConverter
8+
9+
Elem = type_member(:out)
10+
11+
sig { returns(String) }
12+
attr_reader :description
13+
end
14+
end
15+
end
16+
end
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# typed: strong
2+
3+
module OpenAI
4+
module Helpers
5+
module StructuredOutput
6+
# Represents a response from OpenAI's API where the model's output has been structured according to a schema predefined by the user.
7+
#
8+
# This class is specifically used when making requests with the `response_format` parameter set to use structured output (e.g., JSON).
9+
#
10+
# See {examples/structured_outputs_chat_completions.rb} for a complete example of use
11+
class BaseModel < OpenAI::Internal::Type::BaseModel
12+
extend OpenAI::Helpers::StructuredOutput::JsonSchemaConverter
13+
14+
class << self
15+
sig { returns(T.noreturn) }
16+
def optional
17+
end
18+
end
19+
end
20+
end
21+
end
22+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# typed: strong
2+
3+
module OpenAI
4+
module Helpers
5+
module StructuredOutput
6+
class Boolean < OpenAI::Internal::Type::Boolean
7+
extend OpenAI::Helpers::StructuredOutput::JsonSchemaConverter
8+
end
9+
end
10+
end
11+
end
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# typed: strong
2+
3+
module OpenAI
4+
module Helpers
5+
module StructuredOutput
6+
# @example
7+
# example = OpenAI::EnumOf[:foo, :bar, :zoo]
8+
#
9+
# @example
10+
# example = OpenAI::EnumOf[1, 2, 3]
11+
class EnumOf
12+
include OpenAI::Internal::Type::Enum
13+
include OpenAI::Helpers::StructuredOutput::JsonSchemaConverter
14+
15+
sig do
16+
params(
17+
values: T.any(NilClass, T::Boolean, Integer, Float, Symbol)
18+
).returns(T.attached_class)
19+
end
20+
def self.[](*values)
21+
end
22+
23+
sig do
24+
returns(T::Array[T.any(NilClass, T::Boolean, Integer, Float, Symbol)])
25+
end
26+
attr_reader :values
27+
end
28+
end
29+
end
30+
end
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# typed: strong
2+
3+
module OpenAI
4+
module Helpers
5+
module StructuredOutput
6+
JsonSchema = T.type_alias { OpenAI::Internal::AnyHash }
7+
8+
# To customize the JSON schema conversion for a type, implement the `JsonSchemaConverter` interface.
9+
module JsonSchemaConverter
10+
POINTER = T.let(Object.new.freeze, T.anything)
11+
COUNTER = T.let(Object.new.freeze, T.anything)
12+
13+
Input =
14+
T.type_alias do
15+
T.any(
16+
OpenAI::Helpers::StructuredOutput::JsonSchemaConverter,
17+
T::Class[T.anything]
18+
)
19+
end
20+
State =
21+
T.type_alias do
22+
{ defs: T::Hash[Object, String], path: T::Array[String] }
23+
end
24+
25+
# The exact JSON schema produced is subject to improvement between minor release versions.
26+
sig do
27+
params(
28+
state: OpenAI::Helpers::StructuredOutput::JsonSchemaConverter::State
29+
).returns(OpenAI::Helpers::StructuredOutput::JsonSchema)
30+
end
31+
def to_json_schema_inner(state:)
32+
end
33+
34+
# Internal helpers methods.
35+
class << self
36+
# @api private
37+
sig do
38+
params(
39+
schema: OpenAI::Helpers::StructuredOutput::JsonSchema
40+
).returns(OpenAI::Helpers::StructuredOutput::JsonSchema)
41+
end
42+
def to_nilable(schema)
43+
end
44+
45+
# @api private
46+
sig do
47+
params(
48+
state:
49+
OpenAI::Helpers::StructuredOutput::JsonSchemaConverter::State,
50+
type:
51+
OpenAI::Helpers::StructuredOutput::JsonSchemaConverter::Input,
52+
blk: T.proc.returns(OpenAI::Helpers::StructuredOutput::JsonSchema)
53+
).void
54+
end
55+
def cache_def!(state, type:, &blk)
56+
end
57+
58+
# @api private
59+
sig do
60+
params(
61+
type:
62+
OpenAI::Helpers::StructuredOutput::JsonSchemaConverter::Input
63+
).returns(OpenAI::Helpers::StructuredOutput::JsonSchema)
64+
end
65+
def to_json_schema(type)
66+
end
67+
68+
# @api private
69+
sig do
70+
params(
71+
type:
72+
OpenAI::Helpers::StructuredOutput::JsonSchemaConverter::Input,
73+
state:
74+
OpenAI::Helpers::StructuredOutput::JsonSchemaConverter::State
75+
).returns(OpenAI::Helpers::StructuredOutput::JsonSchema)
76+
end
77+
def to_json_schema_inner(type, state:)
78+
end
79+
end
80+
end
81+
end
82+
end
83+
end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# typed: strong
2+
3+
module OpenAI
4+
module Helpers
5+
module StructuredOutput
6+
# @example
7+
# example = OpenAI::UnionOf[Float, OpenAI::ArrayOf[Integer]]
8+
class UnionOf
9+
include OpenAI::Internal::Type::Union
10+
include OpenAI::Helpers::StructuredOutput::JsonSchemaConverter
11+
12+
sig do
13+
params(
14+
variants:
15+
OpenAI::Helpers::StructuredOutput::JsonSchemaConverter::Input
16+
).returns(T.attached_class)
17+
end
18+
def self.[](*variants)
19+
end
20+
end
21+
end
22+
end
23+
end

rbi/openai/models/chat/chat_completion_message.rbi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ module OpenAI
1818
sig { returns(T.nilable(String)) }
1919
attr_accessor :content
2020

21+
# The parsed contents of the message, if JSON schema is specified.
22+
sig { returns(T.nilable(T.anything)) }
23+
attr_accessor :parsed
24+
2125
# The refusal message generated by the model.
2226
sig { returns(T.nilable(String)) }
2327
attr_accessor :refusal

rbi/openai/models/chat/chat_completion_message_tool_call.rbi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ module OpenAI
8080
sig { returns(String) }
8181
attr_accessor :arguments
8282

83+
# The parsed contents of the arguments.
84+
sig { returns(T.anything) }
85+
attr_accessor :parsed
86+
8387
# The name of the function to call.
8488
sig { returns(String) }
8589
attr_accessor :name

0 commit comments

Comments
 (0)