openai 0.107.0 → 0.108.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +49 -1
- data/examples/chat/tool_runner.rb +33 -0
- data/lib/openai/client.rb +2 -2
- data/lib/openai/helpers/chat/tool_runner.rb +259 -0
- data/lib/openai/helpers/realtime/recovery.rb +8 -2
- data/lib/openai/helpers/responses_websocket/incremental.rb +5 -1
- data/lib/openai/helpers/responses_websocket/session.rb +11 -4
- data/lib/openai/helpers/streaming/chat_completion_resource.rb +52 -0
- data/lib/openai/models/admin/organization/usage_audio_speeches_response.rb +4 -4
- data/lib/openai/models/admin/organization/usage_audio_transcriptions_response.rb +4 -4
- data/lib/openai/models/admin/organization/usage_code_interpreter_sessions_response.rb +4 -4
- data/lib/openai/models/admin/organization/usage_completions_response.rb +4 -4
- data/lib/openai/models/admin/organization/usage_costs_response.rb +4 -4
- data/lib/openai/models/admin/organization/usage_decisions_response.rb +4 -4
- data/lib/openai/models/admin/organization/usage_embeddings_response.rb +4 -4
- data/lib/openai/models/admin/organization/usage_file_search_calls_response.rb +4 -4
- data/lib/openai/models/admin/organization/usage_images_response.rb +4 -4
- data/lib/openai/models/admin/organization/usage_moderations_response.rb +4 -4
- data/lib/openai/models/admin/organization/usage_vector_stores_response.rb +4 -4
- data/lib/openai/models/admin/organization/usage_web_search_calls_response.rb +4 -4
- data/lib/openai/models/audio/voice_consent.rb +58 -0
- data/lib/openai/models/audio/voice_consent_create_params.rb +57 -0
- data/lib/openai/models/audio/voice_consent_delete_params.rb +22 -0
- data/lib/openai/models/audio/voice_consent_deleted.rb +34 -0
- data/lib/openai/models/audio/voice_consent_list_params.rb +42 -0
- data/lib/openai/models/audio/voice_consent_retrieve_params.rb +22 -0
- data/lib/openai/models/audio/voice_consent_update_params.rb +32 -0
- data/lib/openai/resources/audio/voice_consents.rb +163 -0
- data/lib/openai/resources/audio.rb +5 -0
- data/lib/openai/version.rb +1 -1
- data/lib/openai.rb +8 -0
- data/rbi/openai/helpers/chat/tool_runner.rbi +74 -0
- data/rbi/openai/models/admin/organization/usage_audio_speeches_response.rbi +4 -4
- data/rbi/openai/models/admin/organization/usage_audio_transcriptions_response.rbi +4 -4
- data/rbi/openai/models/admin/organization/usage_code_interpreter_sessions_response.rbi +4 -4
- data/rbi/openai/models/admin/organization/usage_completions_response.rbi +4 -4
- data/rbi/openai/models/admin/organization/usage_costs_response.rbi +4 -4
- data/rbi/openai/models/admin/organization/usage_decisions_response.rbi +4 -4
- data/rbi/openai/models/admin/organization/usage_embeddings_response.rbi +4 -4
- data/rbi/openai/models/admin/organization/usage_file_search_calls_response.rbi +4 -4
- data/rbi/openai/models/admin/organization/usage_images_response.rbi +4 -4
- data/rbi/openai/models/admin/organization/usage_moderations_response.rbi +4 -4
- data/rbi/openai/models/admin/organization/usage_vector_stores_response.rbi +4 -4
- data/rbi/openai/models/admin/organization/usage_web_search_calls_response.rbi +4 -4
- data/rbi/openai/models/audio/voice_consent.rbi +86 -0
- data/rbi/openai/models/audio/voice_consent_create_params.rbi +92 -0
- data/rbi/openai/models/audio/voice_consent_delete_params.rbi +53 -0
- data/rbi/openai/models/audio/voice_consent_deleted.rbi +62 -0
- data/rbi/openai/models/audio/voice_consent_list_params.rbi +78 -0
- data/rbi/openai/models/audio/voice_consent_retrieve_params.rbi +53 -0
- data/rbi/openai/models/audio/voice_consent_update_params.rbi +62 -0
- data/rbi/openai/models/response_format_json_schema.rbi +13 -11
- data/rbi/openai/resources/audio/voice_consents.rbi +108 -0
- data/rbi/openai/resources/audio.rbi +4 -0
- data/sig/openai/helpers/chat/tool_runner.rbs +48 -0
- data/sig/openai/models/admin/organization/usage_audio_speeches_response.rbs +6 -6
- data/sig/openai/models/admin/organization/usage_audio_transcriptions_response.rbs +6 -6
- data/sig/openai/models/admin/organization/usage_code_interpreter_sessions_response.rbs +6 -6
- data/sig/openai/models/admin/organization/usage_completions_response.rbs +6 -6
- data/sig/openai/models/admin/organization/usage_costs_response.rbs +6 -6
- data/sig/openai/models/admin/organization/usage_decisions_response.rbs +4 -4
- data/sig/openai/models/admin/organization/usage_embeddings_response.rbs +6 -6
- data/sig/openai/models/admin/organization/usage_file_search_calls_response.rbs +6 -6
- data/sig/openai/models/admin/organization/usage_images_response.rbs +6 -6
- data/sig/openai/models/admin/organization/usage_moderations_response.rbs +6 -6
- data/sig/openai/models/admin/organization/usage_vector_stores_response.rbs +6 -6
- data/sig/openai/models/admin/organization/usage_web_search_calls_response.rbs +6 -6
- data/sig/openai/models/audio/voice_consent.rbs +57 -0
- data/sig/openai/models/audio/voice_consent_create_params.rbs +61 -0
- data/sig/openai/models/audio/voice_consent_delete_params.rbs +25 -0
- data/sig/openai/models/audio/voice_consent_deleted.rbs +31 -0
- data/sig/openai/models/audio/voice_consent_list_params.rbs +57 -0
- data/sig/openai/models/audio/voice_consent_retrieve_params.rbs +25 -0
- data/sig/openai/models/audio/voice_consent_update_params.rbs +36 -0
- data/sig/openai/resources/audio/voice_consents.rbs +38 -0
- data/sig/openai/resources/audio.rbs +2 -0
- metadata +29 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bbaffc5b480678a26851b73b6b49f9f63419801d8bbec633d6ed4f3765fe4717
|
|
4
|
+
data.tar.gz: 127b19570f47781df5644f2217d9405e1ea0e82279b319a7683c59b567cbbd72
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ffe2a7748064e0cb03c1eb2482670a4f581323b02a0275fd9654e1f97ecaf85f9128e064f22b5075af7d615d0e9d4fee73144d57f07ceaa9dcde25a419f7b066
|
|
7
|
+
data.tar.gz: c531be809fb0b7dd69d4cb089eea59680a01dbcc258265e47ef6b16052ffd78eeba8f2ccf094ffaa694cdd2dca602b9ebceffa65e362de1427f51e04219a6192
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.108.0](https://github.com/openai/openai-ruby/compare/v0.107.0...v0.108.0) (2026-10-21)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add a bounded Chat Completions tool runner ([#943](https://github.com/openai/openai-ruby/issues/943)) ([db9f85f](https://github.com/openai/openai-ruby/commit/db9f85f42ac3f3d76678db18941c46e0468267a9))
|
|
9
|
+
* **api:** add custom voice consent management ([#951](https://github.com/openai/openai-ruby/issues/951)) ([fc5256c](https://github.com/openai/openai-ruby/commit/fc5256c4332a1cf5b3d9567874c1d24085b574a1))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* preserve accepted JSON through Responses session helpers ([#939](https://github.com/openai/openai-ruby/issues/939)) ([bdea98a](https://github.com/openai/openai-ruby/commit/bdea98a74c9ea093020c82375259baa6b23468d0))
|
|
15
|
+
* preserve Realtime restore errors if abort fails ([#940](https://github.com/openai/openai-ruby/issues/940)) ([3f09468](https://github.com/openai/openai-ruby/commit/3f094684e1618878a6d4936acf1c970ef426afac))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Chores
|
|
19
|
+
|
|
20
|
+
* **api:** clarify cache-write token totals ([#948](https://github.com/openai/openai-ruby/issues/948)) ([b0cb84d](https://github.com/openai/openai-ruby/commit/b0cb84db1759785b37a49b64b3f5398a63d81b1a))
|
|
21
|
+
* **api:** preserve client configuration compatibility ([#944](https://github.com/openai/openai-ruby/issues/944)) ([4d9bb59](https://github.com/openai/openai-ruby/commit/4d9bb5986fcd13b36634d4396a5745eb9ad1de77))
|
|
22
|
+
* **api:** preserve embedding helper loading ([#946](https://github.com/openai/openai-ruby/issues/946)) ([2965c7f](https://github.com/openai/openai-ruby/commit/2965c7fa3a66eeeb1ef77d37d67f6b73ca4d9701))
|
|
23
|
+
* **api:** preserve SDK compatibility ([#949](https://github.com/openai/openai-ruby/issues/949)) ([38cd9bd](https://github.com/openai/openai-ruby/commit/38cd9bdbdd653470c85a534a0026dac2f2f448f1))
|
|
24
|
+
* enforce zero customizations in generated files ([#950](https://github.com/openai/openai-ruby/issues/950)) ([b87c892](https://github.com/openai/openai-ruby/commit/b87c8928395a12c8e608c1b682e1ba9c1a984c75))
|
|
25
|
+
|
|
3
26
|
## [0.107.0](https://github.com/openai/openai-ruby/compare/v0.106.0...v0.107.0) (2026-10-20)
|
|
4
27
|
|
|
5
28
|
|
data/README.md
CHANGED
|
@@ -15,7 +15,7 @@ To use this gem, install via Bundler by adding the following to your application
|
|
|
15
15
|
<!-- x-release-please-start-version -->
|
|
16
16
|
|
|
17
17
|
```ruby
|
|
18
|
-
gem "openai", "~> 0.
|
|
18
|
+
gem "openai", "~> 0.108.0"
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
<!-- x-release-please-end -->
|
|
@@ -50,6 +50,54 @@ stream.each do |event|
|
|
|
50
50
|
end
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
### Automatic Chat Completions tool runner
|
|
54
|
+
|
|
55
|
+
`client.chat.completions.run_tools` runs your function callbacks, sends their results
|
|
56
|
+
back to the model, and returns the final `ChatCompletion`. It accepts the same
|
|
57
|
+
messages, function-tool definitions (including `OpenAI::BaseModel` schemas), and
|
|
58
|
+
request options as `create`:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
completion = client.chat.completions.run_tools(
|
|
62
|
+
model: "gpt-4o-mini",
|
|
63
|
+
messages: [{role: :user, content: "What is the weather in Seattle?"}],
|
|
64
|
+
tools: [WeatherArguments], # An OpenAI::BaseModel with a city field.
|
|
65
|
+
tool_handlers: {
|
|
66
|
+
"WeatherArguments" => -> (arguments) { {temperature_c: 18, city: arguments.fetch("city")} }
|
|
67
|
+
},
|
|
68
|
+
max_rounds: 10
|
|
69
|
+
)
|
|
70
|
+
puts(completion.choices.first.message.content)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Handlers receive a parsed JSON object with **string keys**; validate application
|
|
74
|
+
requirements in your callback. String results are sent unchanged; other results
|
|
75
|
+
are JSON encoded. Callbacks execute sequentially, including when the model requests
|
|
76
|
+
multiple tools in one response. Setting `parallel_tool_calls: false` limits each
|
|
77
|
+
response to one tool call; the runner rejects larger batches before callbacks run.
|
|
78
|
+
Every advertised tool must have a matching handler.
|
|
79
|
+
The runner validates each batch before invoking callbacks, rejects repeated call
|
|
80
|
+
IDs, enforces the initial `tool_choice` before returning or invoking callbacks, and
|
|
81
|
+
enforces the `tool_choice.allowed_tools.tools` subset on every round. It
|
|
82
|
+
does not mutate your input messages or append the conversation to them.
|
|
83
|
+
|
|
84
|
+
This helper is non-streaming and supports one completion choice and function tools.
|
|
85
|
+
`max_rounds` limits API calls, including the final answer; it raises
|
|
86
|
+
`OpenAI::Helpers::Chat::ToolRunner::MaxRoundsError` before executing tools in the
|
|
87
|
+
last allowed round. A forced `tool_choice` applies to the first round, then switches
|
|
88
|
+
to `auto` while preserving any allowed-tools list. Other invalid or incomplete tool
|
|
89
|
+
responses raise `OpenAI::Helpers::Chat::ToolRunner::Error`.
|
|
90
|
+
|
|
91
|
+
Callback, transport, and API errors stop the run and propagate to your application.
|
|
92
|
+
Callback exception text is never submitted as a tool result, and HTTP retries do
|
|
93
|
+
not rerun callbacks. Earlier callback side effects are not rolled back; starting another run
|
|
94
|
+
can execute tools again. Use application-level idempotency where needed. Request
|
|
95
|
+
timeouts apply per API call, not to callback execution or the entire run.
|
|
96
|
+
Caller-supplied request idempotency keys are scoped to each round, so retries keep
|
|
97
|
+
the same key without colliding with the next round.
|
|
98
|
+
|
|
99
|
+
See [the complete example](examples/chat/tool_runner.rb).
|
|
100
|
+
|
|
53
101
|
### Agents final output (beta)
|
|
54
102
|
|
|
55
103
|
Collect final output from an Agents creation stream:
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../../lib/openai"
|
|
5
|
+
|
|
6
|
+
class WeatherArguments < OpenAI::BaseModel
|
|
7
|
+
required :city, String
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
client = OpenAI::Client.new
|
|
11
|
+
calls = 0
|
|
12
|
+
completion = client.chat.completions.run_tools(
|
|
13
|
+
model: "gpt-4o-mini",
|
|
14
|
+
messages: [{role: :user, content: "What is the weather in Seattle? Use the provided weather tool."}],
|
|
15
|
+
tools: [WeatherArguments],
|
|
16
|
+
tool_choice: {type: :function, function: {name: "WeatherArguments"}},
|
|
17
|
+
tool_handlers: {
|
|
18
|
+
"WeatherArguments" => -> (arguments) do
|
|
19
|
+
calls += 1
|
|
20
|
+
# Replace this synthetic result with your weather service.
|
|
21
|
+
{city: arguments.fetch("city"), temperature_c: 18, condition: "rain"}
|
|
22
|
+
end
|
|
23
|
+
},
|
|
24
|
+
max_rounds: 5
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
unless calls.positive? &&
|
|
28
|
+
completion.choices.first.finish_reason == :stop &&
|
|
29
|
+
!completion.choices.first.message.content.to_s.empty?
|
|
30
|
+
abort("Expected a weather callback and a final answer")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
puts("Tool runner completed.")
|
data/lib/openai/client.rb
CHANGED
|
@@ -206,8 +206,8 @@ module OpenAI
|
|
|
206
206
|
# @param base_url [String, nil] Override the default base URL for the API, e.g.,
|
|
207
207
|
# `"https://api.example.com/v2/"`. Defaults to `ENV["OPENAI_BASE_URL"]`
|
|
208
208
|
#
|
|
209
|
-
# @param data_residency [Symbol, String, nil] Select `global`, `us`, `eu`,
|
|
210
|
-
#
|
|
209
|
+
# @param data_residency [Symbol, String, nil] Select `global`, `us`, `eu`, `ae`.
|
|
210
|
+
# Mutually exclusive with an explicit `base_url` or `provider`.
|
|
211
211
|
# This selects an endpoint; project and model eligibility still apply.
|
|
212
212
|
#
|
|
213
213
|
# @param default_headers [Hash{String=>String, nil}, nil] Extra headers to send
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../internal/request_options_scope"
|
|
4
|
+
|
|
5
|
+
module OpenAI
|
|
6
|
+
module Helpers
|
|
7
|
+
module Chat
|
|
8
|
+
# Executes a bounded, non-streaming Chat Completions function-tool loop.
|
|
9
|
+
class ToolRunner
|
|
10
|
+
class Error < StandardError
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class MaxRoundsError < Error
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @api private
|
|
17
|
+
def initialize(completions:, params:, tool_handlers:, max_rounds:)
|
|
18
|
+
unless max_rounds.is_a?(Integer) && max_rounds.positive?
|
|
19
|
+
raise ArgumentError, "max_rounds must be a positive integer"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@completions = completions
|
|
23
|
+
@max_rounds = max_rounds
|
|
24
|
+
@params, options = OpenAI::Chat::CompletionCreateParams.dump_request(params)
|
|
25
|
+
@request_options = OpenAI::Internal::RequestOptionsScope.new(options)
|
|
26
|
+
@handlers = tool_handlers.to_h.dup
|
|
27
|
+
validate_options(options)
|
|
28
|
+
|
|
29
|
+
# Validate a separate copy: schema conversion must not replace the models
|
|
30
|
+
# used by create to parse the final completion's structured output.
|
|
31
|
+
definitions, = OpenAI::Chat::CompletionCreateParams.dump_request(params)
|
|
32
|
+
completions.get_structured_output_models(definitions)
|
|
33
|
+
names = definitions.fetch(:tools).map do |tool|
|
|
34
|
+
unless tool[:type] == :function || tool[:type] == "function"
|
|
35
|
+
raise ArgumentError, "run_tools supports function tools only"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
name = tool.dig(:function, :name)
|
|
39
|
+
unless name.is_a?(String) && !name.empty? && @handlers[name].respond_to?(:call)
|
|
40
|
+
raise ArgumentError, "Each function tool must have a named callable in tool_handlers"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
name
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
unless names.uniq.length == names.length && @handlers.keys.sort == names.sort
|
|
47
|
+
raise ArgumentError, "Tool definitions and tool_handlers must have matching, unique string names"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@allowed_names = allowed_tool_names(names)
|
|
51
|
+
@seen = {}
|
|
52
|
+
@params.fetch(:messages).each do |message|
|
|
53
|
+
Array(message[:tool_calls]).each { |call| @seen[call[:id]] = true }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @api private
|
|
58
|
+
def run
|
|
59
|
+
@max_rounds.times do |round|
|
|
60
|
+
completion = create_completion(round)
|
|
61
|
+
assistant, prepared = prepare_completion(completion, round)
|
|
62
|
+
return completion unless assistant
|
|
63
|
+
|
|
64
|
+
@params[:messages] << assistant
|
|
65
|
+
prepared.each do |call, handler, arguments|
|
|
66
|
+
output = handler.call(arguments)
|
|
67
|
+
content = output.is_a?(String) ? output.dup : JSON.generate(output)
|
|
68
|
+
@params[:messages] << {role: :tool, tool_call_id: call[:id], content: content}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# A forced tool choice applies to the first round; allow a final answer.
|
|
72
|
+
# Preserve the caller's allowed-tools restriction, when supplied.
|
|
73
|
+
choice = @params[:tool_choice]
|
|
74
|
+
@params[:tool_choice] = if choice.is_a?(Hash) && choice[:type].to_s == "allowed_tools"
|
|
75
|
+
choice.merge(allowed_tools: choice.fetch(:allowed_tools).merge(mode: :auto))
|
|
76
|
+
else
|
|
77
|
+
:auto
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def create_completion(round)
|
|
85
|
+
@completions.create_for_tool_runner(
|
|
86
|
+
@params.merge(request_options: @request_options.child("chat.tool_runner.round.#{round}"))
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Keep response validation separate from transport and callback execution.
|
|
91
|
+
def prepare_completion(completion, round)
|
|
92
|
+
unless completion.choices.length == 1
|
|
93
|
+
raise Error, "run_tools requires exactly one completion choice"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
choice = completion.choices.first
|
|
97
|
+
message = choice.message
|
|
98
|
+
unless message.to_h.key?(:role) && message.role == :assistant
|
|
99
|
+
raise Error, "Expected an assistant message"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
[:content, :refusal].each do |field|
|
|
103
|
+
value = message[field]
|
|
104
|
+
unless message.to_h.key?(field) && (value.nil? || value.is_a?(String))
|
|
105
|
+
raise Error, "Expected assistant content and refusal to be strings or null"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
audio = message.audio
|
|
110
|
+
if audio && (!audio[:id].is_a?(String) || audio[:id].empty?)
|
|
111
|
+
raise Error, "Expected an audio reference with a nonempty string ID"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
calls = message.tool_calls || []
|
|
115
|
+
if @params[:parallel_tool_calls] == false && calls.length > 1
|
|
116
|
+
raise Error, "Model returned multiple tool calls with parallel_tool_calls disabled"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
validate_tool_choice(calls)
|
|
120
|
+
if choice.finish_reason == :stop && calls.empty? && message.function_call.nil?
|
|
121
|
+
return [nil, nil]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
unless [:stop, :tool_calls].include?(choice.finish_reason) && !calls.empty? && message.function_call.nil?
|
|
125
|
+
raise Error, "Completion did not finish normally or provide complete function tool calls"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
if round + 1 == @max_rounds
|
|
129
|
+
raise MaxRoundsError, "Tool runner reached max_rounds before a final completion"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Prepare the entire batch before allowing any callback side effects.
|
|
133
|
+
prepared = calls.map { |call| prepare(call) }
|
|
134
|
+
assistant = message.to_h.slice(:role, :content, :refusal)
|
|
135
|
+
assistant[:tool_calls] = prepared.map { |call, _handler, _arguments| call }
|
|
136
|
+
assistant[:audio] = {id: audio.id} if audio
|
|
137
|
+
[assistant, prepared]
|
|
138
|
+
rescue KeyError, NoMethodError, TypeError, OpenAI::Errors::ConversionError
|
|
139
|
+
raise Error, "Malformed Chat Completion response", cause: nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def validate_tool_choice(calls)
|
|
143
|
+
choice = @params[:tool_choice]
|
|
144
|
+
if choice == :none || choice == "none"
|
|
145
|
+
raise Error, "Model requested tools when tool_choice is none" unless calls.empty?
|
|
146
|
+
return
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
required = choice == :required || choice == "required"
|
|
150
|
+
if choice.is_a?(Hash)
|
|
151
|
+
case (choice[:type] || choice["type"]).to_s
|
|
152
|
+
when "allowed_tools"
|
|
153
|
+
required = choice.fetch(:allowed_tools).fetch(:mode).to_s == "required"
|
|
154
|
+
when "function"
|
|
155
|
+
required = true
|
|
156
|
+
function = choice[:function] || choice["function"]
|
|
157
|
+
name = function[:name] || function["name"]
|
|
158
|
+
unless calls.all? { |call| call.function.name == name }
|
|
159
|
+
raise Error, "Model did not call the forced function tool"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
raise Error, "Model returned no tool calls for a required tool choice" if required && calls.empty?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def allowed_tool_names(names)
|
|
168
|
+
choice = @params[:tool_choice]
|
|
169
|
+
return names unless choice.is_a?(Hash) && (choice[:type] || choice["type"]).to_s == "allowed_tools"
|
|
170
|
+
|
|
171
|
+
choice = OpenAI::Chat::ChatCompletionAllowedToolChoice.dump(choice, state: {can_retry: true})
|
|
172
|
+
@params[:tool_choice] = choice
|
|
173
|
+
allowed = choice[:allowed_tools]
|
|
174
|
+
unless allowed.is_a?(Hash) && allowed[:tools].is_a?(Array)
|
|
175
|
+
raise ArgumentError, "allowed_tools must include a tools array"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
allowed[:tools].map do |tool|
|
|
179
|
+
type = nil
|
|
180
|
+
function = nil
|
|
181
|
+
name = nil
|
|
182
|
+
type = tool[:type] || tool["type"] if tool.is_a?(Hash)
|
|
183
|
+
function = tool[:function] || tool["function"] if tool.is_a?(Hash)
|
|
184
|
+
name = function[:name] || function["name"] if function.is_a?(Hash)
|
|
185
|
+
unless type.to_s == "function" && names.include?(name)
|
|
186
|
+
raise ArgumentError, "allowed_tools must reference registered function tools"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
name
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def validate_options(options)
|
|
194
|
+
unless @params[:n].nil? || @params[:n] == 1
|
|
195
|
+
raise ArgumentError, "run_tools supports n: 1 only"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
if @params[:stream] || @params[:functions] || @params[:function_call]
|
|
199
|
+
raise ArgumentError, "run_tools requires non-streaming function tools"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
controlled = %w[messages tools tool_choice parallel_tool_calls n stream functions function_call]
|
|
203
|
+
if options[:extra_body]&.keys&.any? { |key| controlled.include?(key.to_s) }
|
|
204
|
+
raise ArgumentError, "extra_body cannot override tool runner control parameters"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
unless @params[:messages].is_a?(Array) && @params[:tools].is_a?(Array)
|
|
208
|
+
raise ArgumentError, "messages and tools must be arrays"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
unless @handlers.keys.all? { |name| name.is_a?(String) }
|
|
212
|
+
raise ArgumentError, "tool_handlers keys must be strings"
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def prepare(call)
|
|
217
|
+
unless call.is_a?(OpenAI::Chat::ChatCompletionMessageFunctionToolCall) &&
|
|
218
|
+
[:function, "function"].include?(call[:type]) &&
|
|
219
|
+
call[:id].is_a?(String) &&
|
|
220
|
+
!call.id.empty? &&
|
|
221
|
+
!@seen.key?(call.id)
|
|
222
|
+
raise Error, "Expected a function tool call with a new, nonempty ID"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
function = call.function
|
|
226
|
+
unless function.is_a?(OpenAI::Chat::ChatCompletionMessageFunctionToolCall::Function) &&
|
|
227
|
+
function[:name].is_a?(String) &&
|
|
228
|
+
function[:arguments].is_a?(String)
|
|
229
|
+
raise Error, "Expected a function tool payload"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
handler = @handlers[function.name]
|
|
233
|
+
raise Error, "Model requested an unregistered function tool" unless handler
|
|
234
|
+
unless @allowed_names.include?(function.name)
|
|
235
|
+
raise Error, "Model requested a function tool that is not allowed"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
arguments = begin
|
|
239
|
+
JSON.parse(function.arguments)
|
|
240
|
+
rescue JSON::ParserError, TypeError
|
|
241
|
+
raise Error, "Function arguments must be a valid JSON object", cause: nil
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
raise Error, "Function arguments must be a JSON object" unless arguments.is_a?(Hash)
|
|
245
|
+
|
|
246
|
+
@seen[call.id] = true
|
|
247
|
+
wire_call = {
|
|
248
|
+
id: call.id,
|
|
249
|
+
type: :function,
|
|
250
|
+
function: {name: function.name, arguments: function.arguments}
|
|
251
|
+
}
|
|
252
|
+
[wire_call, handler, arguments]
|
|
253
|
+
rescue OpenAI::Errors::ConversionError
|
|
254
|
+
raise Error, "Malformed function tool payload", cause: nil
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
@@ -306,8 +306,14 @@ module OpenAI
|
|
|
306
306
|
# Shutdown must not mask the operation's result or prevent cleanup.
|
|
307
307
|
nil
|
|
308
308
|
ensure
|
|
309
|
-
|
|
310
|
-
|
|
309
|
+
pending_error = $ERROR_INFO
|
|
310
|
+
begin
|
|
311
|
+
physical.abort
|
|
312
|
+
rescue StandardError
|
|
313
|
+
raise if pending_error.nil?
|
|
314
|
+
ensure
|
|
315
|
+
@physical = nil
|
|
316
|
+
end
|
|
311
317
|
end
|
|
312
318
|
end
|
|
313
319
|
end
|
|
@@ -11,6 +11,8 @@ module OpenAI
|
|
|
11
11
|
# are retained exactly as received, never filled from provisional output.
|
|
12
12
|
# Call reset to discard all retained state.
|
|
13
13
|
class IncrementalResponse
|
|
14
|
+
include OpenAI::WebSocket::Protocol
|
|
15
|
+
|
|
14
16
|
attr_reader :phase
|
|
15
17
|
|
|
16
18
|
def initialize
|
|
@@ -236,10 +238,12 @@ module OpenAI
|
|
|
236
238
|
# references. Returned snapshots and incoming events must not share strings.
|
|
237
239
|
def copy(value)
|
|
238
240
|
data = OpenAI::Internal::Type::BaseModel.recursively_to_h(value, convert: false)
|
|
239
|
-
data =
|
|
241
|
+
data = parse_json(generate_json(data))
|
|
240
242
|
return data unless value.is_a?(OpenAI::Internal::Type::BaseModel)
|
|
241
243
|
|
|
242
244
|
OpenAI::Internal::Type::Converter.coerce(value.class, data)
|
|
245
|
+
rescue SystemStackError
|
|
246
|
+
raise SessionError, "Could not copy the Responses WebSocket event.", cause: nil
|
|
243
247
|
end
|
|
244
248
|
end
|
|
245
249
|
end
|
|
@@ -55,6 +55,8 @@ module OpenAI
|
|
|
55
55
|
|
|
56
56
|
# @api private
|
|
57
57
|
class SessionAccumulator
|
|
58
|
+
include OpenAI::WebSocket::Protocol
|
|
59
|
+
|
|
58
60
|
attr_reader :final_response, :error
|
|
59
61
|
|
|
60
62
|
def initialize(limit)
|
|
@@ -93,10 +95,10 @@ module OpenAI
|
|
|
93
95
|
if type == "response.output_item.done"
|
|
94
96
|
# Retain wire data independently of the mutable event returned by receive.
|
|
95
97
|
completed_item = OpenAI::Internal::Type::BaseModel.recursively_to_h(event.item, convert: false)
|
|
96
|
-
@items[event.output_index] =
|
|
98
|
+
@items[event.output_index] = parse_json(generate_json(completed_item))
|
|
97
99
|
elsif %w[response.completed response.failed response.incomplete].include?(type)
|
|
98
100
|
data = OpenAI::Internal::Type::BaseModel.recursively_to_h(event, convert: false)
|
|
99
|
-
data =
|
|
101
|
+
data = parse_json(generate_json(data))
|
|
100
102
|
response = data.fetch(:response)
|
|
101
103
|
if response[:output].nil?
|
|
102
104
|
response = response.merge(output: @items.sort.map { |_index, item| item.to_h })
|
|
@@ -117,7 +119,7 @@ module OpenAI
|
|
|
117
119
|
@state.handle_event(@decoder.decode(data))
|
|
118
120
|
end
|
|
119
121
|
|
|
120
|
-
rescue StandardError => error
|
|
122
|
+
rescue StandardError, SystemStackError => error
|
|
121
123
|
@error = error.is_a?(SessionError) ? error : SessionError.new("Could not collect the Responses final response.")
|
|
122
124
|
clear
|
|
123
125
|
end
|
|
@@ -313,6 +315,8 @@ module OpenAI
|
|
|
313
315
|
|
|
314
316
|
# Owns one connection and one Async reader. Use only its lanes to receive.
|
|
315
317
|
class Session
|
|
318
|
+
include OpenAI::WebSocket::Protocol
|
|
319
|
+
|
|
316
320
|
attr_reader :limits, :default
|
|
317
321
|
|
|
318
322
|
def self.open(
|
|
@@ -531,7 +535,7 @@ module OpenAI
|
|
|
531
535
|
value = @lanes.fetch(data[:stream_id], @lanes[nil])
|
|
532
536
|
return unless value
|
|
533
537
|
|
|
534
|
-
bytes =
|
|
538
|
+
bytes = generate_json(data).bytesize
|
|
535
539
|
if @events >= @limits.max_events || @bytes + bytes > @limits.max_bytes
|
|
536
540
|
raise BufferError, "The Responses session exceeded its buffered event budget."
|
|
537
541
|
end
|
|
@@ -544,6 +548,9 @@ module OpenAI
|
|
|
544
548
|
consumed(bytes)
|
|
545
549
|
raise
|
|
546
550
|
end
|
|
551
|
+
|
|
552
|
+
rescue SystemStackError
|
|
553
|
+
raise SessionError, "Could not buffer the Responses WebSocket event.", cause: nil
|
|
547
554
|
end
|
|
548
555
|
|
|
549
556
|
private def stop_reader
|
|
@@ -1,9 +1,61 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../chat/tool_runner"
|
|
4
|
+
|
|
3
5
|
module OpenAI
|
|
4
6
|
module Resources
|
|
5
7
|
class Chat
|
|
6
8
|
class Completions
|
|
9
|
+
# Run function tools until the model returns a final, non-streaming completion.
|
|
10
|
+
# Handlers receive a JSON object with string keys. String results are sent
|
|
11
|
+
# verbatim; other results are JSON encoded. Callback errors propagate without
|
|
12
|
+
# retrying callbacks or sending their exception text to the model.
|
|
13
|
+
# max_rounds bounds API calls (including the final answer), defaulting to 10.
|
|
14
|
+
# A forced tool choice applies only to the first round. Callbacks run serially.
|
|
15
|
+
#
|
|
16
|
+
# @param tool_handlers [Hash{String=>#call}]
|
|
17
|
+
# @param max_rounds [Integer]
|
|
18
|
+
# @param params [Hash] Other Chat Completions create parameters.
|
|
19
|
+
# @return [OpenAI::Models::Chat::ChatCompletion]
|
|
20
|
+
def run_tools(model:, messages:, tools:, tool_handlers:, max_rounds: 10, **params)
|
|
21
|
+
OpenAI::Helpers::Chat::ToolRunner
|
|
22
|
+
.new(
|
|
23
|
+
completions: self,
|
|
24
|
+
params: params.merge(model: model, messages: messages, tools: tools),
|
|
25
|
+
tool_handlers: tool_handlers,
|
|
26
|
+
max_rounds: max_rounds
|
|
27
|
+
)
|
|
28
|
+
.run
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @api private
|
|
32
|
+
# Buffer through the normal transport, then decode inside the runner's
|
|
33
|
+
# error boundary. Transport exceptions remain outside that boundary.
|
|
34
|
+
def create_for_tool_runner(params)
|
|
35
|
+
parsed, options = OpenAI::Chat::CompletionCreateParams.dump_request(params)
|
|
36
|
+
model, tool_models = get_structured_output_models(parsed)
|
|
37
|
+
parse = OpenAI::Helpers::StructuredOutput::ChatCompletionParser.build_unwrap(model, tool_models)
|
|
38
|
+
response = @client.request(
|
|
39
|
+
method: :post,
|
|
40
|
+
path: "chat/completions",
|
|
41
|
+
body: parsed,
|
|
42
|
+
model: StringIO,
|
|
43
|
+
security: {bearer_auth: true},
|
|
44
|
+
options: options
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
begin
|
|
48
|
+
raw = JSON.parse(response.string, symbolize_names: true)
|
|
49
|
+
completion = OpenAI::Internal::Type::Converter.coerce(OpenAI::Chat::ChatCompletion, parse.call(raw))
|
|
50
|
+
completion._set_last_response(response.last_response)
|
|
51
|
+
completion
|
|
52
|
+
rescue JSON::ParserError, KeyError, NoMethodError, TypeError, OpenAI::Errors::ConversionError
|
|
53
|
+
raise OpenAI::Helpers::Chat::ToolRunner::Error, "Malformed Chat Completion response", cause: nil
|
|
54
|
+
ensure
|
|
55
|
+
response.close
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
7
59
|
def stream(params)
|
|
8
60
|
parsed, options = OpenAI::Chat::CompletionCreateParams.dump_request(params)
|
|
9
61
|
|
|
@@ -243,8 +243,8 @@ module OpenAI
|
|
|
243
243
|
optional :input_cache_write_30m_tokens, Integer, nil?: true
|
|
244
244
|
|
|
245
245
|
# @!attribute input_cache_write_tokens
|
|
246
|
-
# The aggregated number of input tokens written to the cache
|
|
247
|
-
# retention
|
|
246
|
+
# The aggregated number of input tokens written to the cache across 30-minute and
|
|
247
|
+
# 12-hour retention periods.
|
|
248
248
|
#
|
|
249
249
|
# @return [Integer, nil]
|
|
250
250
|
optional :input_cache_write_tokens, Integer
|
|
@@ -382,8 +382,8 @@ module OpenAI
|
|
|
382
382
|
# retention period. May be absent or null for older responses.
|
|
383
383
|
#
|
|
384
384
|
# @param input_cache_write_tokens [Integer]
|
|
385
|
-
# The aggregated number of input tokens written to the cache
|
|
386
|
-
# retention
|
|
385
|
+
# The aggregated number of input tokens written to the cache across 30-minute and
|
|
386
|
+
# 12-hour retention periods.
|
|
387
387
|
#
|
|
388
388
|
# @param input_cached_audio_tokens [Integer]
|
|
389
389
|
# The aggregated number of cached audio input tokens used.
|
|
@@ -245,8 +245,8 @@ module OpenAI
|
|
|
245
245
|
optional :input_cache_write_30m_tokens, Integer, nil?: true
|
|
246
246
|
|
|
247
247
|
# @!attribute input_cache_write_tokens
|
|
248
|
-
# The aggregated number of input tokens written to the cache
|
|
249
|
-
# retention
|
|
248
|
+
# The aggregated number of input tokens written to the cache across 30-minute and
|
|
249
|
+
# 12-hour retention periods.
|
|
250
250
|
#
|
|
251
251
|
# @return [Integer, nil]
|
|
252
252
|
optional :input_cache_write_tokens, Integer
|
|
@@ -384,8 +384,8 @@ module OpenAI
|
|
|
384
384
|
# retention period. May be absent or null for older responses.
|
|
385
385
|
#
|
|
386
386
|
# @param input_cache_write_tokens [Integer]
|
|
387
|
-
# The aggregated number of input tokens written to the cache
|
|
388
|
-
# retention
|
|
387
|
+
# The aggregated number of input tokens written to the cache across 30-minute and
|
|
388
|
+
# 12-hour retention periods.
|
|
389
389
|
#
|
|
390
390
|
# @param input_cached_audio_tokens [Integer]
|
|
391
391
|
# The aggregated number of cached audio input tokens used.
|
|
@@ -245,8 +245,8 @@ module OpenAI
|
|
|
245
245
|
optional :input_cache_write_30m_tokens, Integer, nil?: true
|
|
246
246
|
|
|
247
247
|
# @!attribute input_cache_write_tokens
|
|
248
|
-
# The aggregated number of input tokens written to the cache
|
|
249
|
-
# retention
|
|
248
|
+
# The aggregated number of input tokens written to the cache across 30-minute and
|
|
249
|
+
# 12-hour retention periods.
|
|
250
250
|
#
|
|
251
251
|
# @return [Integer, nil]
|
|
252
252
|
optional :input_cache_write_tokens, Integer
|
|
@@ -384,8 +384,8 @@ module OpenAI
|
|
|
384
384
|
# retention period. May be absent or null for older responses.
|
|
385
385
|
#
|
|
386
386
|
# @param input_cache_write_tokens [Integer]
|
|
387
|
-
# The aggregated number of input tokens written to the cache
|
|
388
|
-
# retention
|
|
387
|
+
# The aggregated number of input tokens written to the cache across 30-minute and
|
|
388
|
+
# 12-hour retention periods.
|
|
389
389
|
#
|
|
390
390
|
# @param input_cached_audio_tokens [Integer]
|
|
391
391
|
# The aggregated number of cached audio input tokens used.
|
|
@@ -241,8 +241,8 @@ module OpenAI
|
|
|
241
241
|
optional :input_cache_write_30m_tokens, Integer, nil?: true
|
|
242
242
|
|
|
243
243
|
# @!attribute input_cache_write_tokens
|
|
244
|
-
# The aggregated number of input tokens written to the cache
|
|
245
|
-
# retention
|
|
244
|
+
# The aggregated number of input tokens written to the cache across 30-minute and
|
|
245
|
+
# 12-hour retention periods.
|
|
246
246
|
#
|
|
247
247
|
# @return [Integer, nil]
|
|
248
248
|
optional :input_cache_write_tokens, Integer
|
|
@@ -380,8 +380,8 @@ module OpenAI
|
|
|
380
380
|
# retention period. May be absent or null for older responses.
|
|
381
381
|
#
|
|
382
382
|
# @param input_cache_write_tokens [Integer]
|
|
383
|
-
# The aggregated number of input tokens written to the cache
|
|
384
|
-
# retention
|
|
383
|
+
# The aggregated number of input tokens written to the cache across 30-minute and
|
|
384
|
+
# 12-hour retention periods.
|
|
385
385
|
#
|
|
386
386
|
# @param input_cached_audio_tokens [Integer]
|
|
387
387
|
# The aggregated number of cached audio input tokens used.
|