openai 0.106.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 +74 -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/live/websocket.rb +12 -141
- data/lib/openai/helpers/local_audio/process.rb +13 -3
- data/lib/openai/helpers/models/embedding.rb +42 -0
- data/lib/openai/helpers/realtime/connection.rb +22 -5
- data/lib/openai/helpers/realtime/recovery.rb +19 -7
- data/lib/openai/helpers/realtime/translation.rb +37 -169
- data/lib/openai/helpers/realtime/unknown_server_event.rb +5 -12
- data/lib/openai/helpers/responses_websocket/connection.rb +56 -35
- data/lib/openai/helpers/responses_websocket/incremental.rb +5 -1
- data/lib/openai/helpers/responses_websocket/session.rb +11 -4
- data/lib/openai/helpers/responses_websocket/unknown_server_event.rb +2 -14
- data/lib/openai/helpers/streaming/chat_completion_resource.rb +52 -0
- data/lib/openai/helpers/websocket/async_websocket_transport.rb +8 -6
- data/lib/openai/helpers/websocket/client_request.rb +1 -1
- data/lib/openai/helpers/websocket/connection.rb +21 -10
- data/lib/openai/helpers/websocket/connection_manager.rb +2 -1
- data/lib/openai/helpers/websocket/protocol.rb +160 -0
- data/lib/openai/helpers/websocket/typed_connection.rb +22 -16
- data/lib/openai/helpers/websocket/typed_resource.rb +15 -0
- data/lib/openai/internal/transport/base_client.rb +1 -1
- data/lib/openai/models/admin/organization/usage_audio_speeches_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_audio_transcriptions_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_code_interpreter_sessions_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_completions_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_costs_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_decisions_response.rb +14 -3
- data/lib/openai/models/admin/organization/usage_embeddings_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_file_search_calls_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_images_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_moderations_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_vector_stores_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_web_search_calls_response.rb +16 -5
- 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/models/beta/beta_response_input_item.rb +10 -1
- data/lib/openai/models/responses/response_input_item.rb +10 -1
- 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 +9 -0
- data/rbi/openai/client.rbi +1 -8
- data/rbi/openai/helpers/chat/tool_runner.rbi +74 -0
- data/rbi/openai/helpers/realtime/translation.rbi +0 -5
- data/rbi/openai/helpers/realtime/unknown_server_event.rbi +6 -0
- data/rbi/openai/helpers/websocket.rbi +44 -6
- data/rbi/openai/internal/transport/base_client.rbi +2 -6
- data/rbi/openai/models/admin/organization/usage_audio_speeches_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_audio_transcriptions_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_code_interpreter_sessions_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_completions_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_costs_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_decisions_response.rbi +12 -0
- data/rbi/openai/models/admin/organization/usage_embeddings_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_file_search_calls_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_images_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_moderations_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_vector_stores_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_web_search_calls_response.rbi +14 -2
- 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/beta/beta_response_input_item.rbi +14 -1
- data/rbi/openai/models/response_format_json_schema.rbi +13 -11
- data/rbi/openai/models/responses/response_input_item.rbi +10 -1
- data/rbi/openai/resources/audio/voice_consents.rbi +108 -0
- data/rbi/openai/resources/audio.rbi +4 -0
- data/realtime.md +22 -3
- data/sig/openai/client.rbs +1 -1
- data/sig/openai/helpers/chat/tool_runner.rbs +48 -0
- data/sig/openai/helpers/realtime/translation.rbs +0 -2
- data/sig/openai/helpers/realtime/unknown_server_event.rbs +2 -0
- data/sig/openai/helpers/websocket.rbs +23 -6
- data/sig/openai/models/admin/organization/usage_audio_speeches_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_audio_transcriptions_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_code_interpreter_sessions_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_completions_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_costs_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_decisions_response.rbs +11 -1
- data/sig/openai/models/admin/organization/usage_embeddings_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_file_search_calls_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_images_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_moderations_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_vector_stores_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_web_search_calls_response.rbs +15 -5
- 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/models/beta/beta_response_input_item.rbs +8 -0
- data/sig/openai/models/responses/response_input_item.rbs +8 -3
- data/sig/openai/resources/audio/voice_consents.rbs +38 -0
- data/sig/openai/resources/audio.rbs +2 -0
- metadata +31 -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,79 @@
|
|
|
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
|
+
|
|
26
|
+
## [0.107.0](https://github.com/openai/openai-ruby/compare/v0.106.0...v0.107.0) (2026-10-20)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Features
|
|
30
|
+
|
|
31
|
+
* **api:** add 30-minute cache-write usage counts ([#915](https://github.com/openai/openai-ruby/issues/915)) ([ea2dd66](https://github.com/openai/openai-ruby/commit/ea2dd66ed0c16e544490332fc2ea8ec4f5abd1bb))
|
|
32
|
+
* **api:** add compaction trigger IDs ([#934](https://github.com/openai/openai-ruby/issues/934)) ([224e04a](https://github.com/openai/openai-ruby/commit/224e04ac2f610c793d772efafe7244d6c88039fc))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
### Bug Fixes
|
|
36
|
+
|
|
37
|
+
* **api:** correct authentication security key types ([#938](https://github.com/openai/openai-ruby/issues/938)) ([4a5cc74](https://github.com/openai/openai-ruby/commit/4a5cc744b38dc39042d98ae2ff5a32fdd0247bc9))
|
|
38
|
+
* decode base64 embedding vectors ([#942](https://github.com/openai/openai-ruby/issues/942)) ([d684df4](https://github.com/openai/openai-ruby/commit/d684df4dc2a633e34836a346bbbe1e7cbb3d2707))
|
|
39
|
+
* drain audio diagnostics while waiting on input ([#920](https://github.com/openai/openai-ruby/issues/920)) ([8a0b876](https://github.com/openai/openai-ruby/commit/8a0b87606e55f26e86e289577cdfd2533c329ad6))
|
|
40
|
+
* drain ready audio diagnostics without delay ([#918](https://github.com/openai/openai-ruby/issues/918)) ([3e87ce3](https://github.com/openai/openai-ruby/commit/3e87ce380fafe08bb4348b9b867d4006bb939e49))
|
|
41
|
+
* keep Realtime diagnostic rendering payload-free ([#924](https://github.com/openai/openai-ruby/issues/924)) ([3a29026](https://github.com/openai/openai-ruby/commit/3a29026ab177704dbe30d688fddd085e737dfc6d))
|
|
42
|
+
* preserve deep JSON in WebSocket event codecs ([#928](https://github.com/openai/openai-ruby/issues/928)) ([57462c9](https://github.com/openai/openai-ruby/commit/57462c938e3b7602c456b0816e8873bd67fad241))
|
|
43
|
+
* preserve typed Realtime event identity ([#931](https://github.com/openai/openai-ruby/issues/931)) ([9bb5618](https://github.com/openai/openai-ruby/commit/9bb56187c7eb0d72f992c76fed68369d34972d81))
|
|
44
|
+
* **realtime:** abort connections after failed or canceled WebSocket writes so buffered application data cannot be flushed later. On a poisoned connection, `close(code:, reason:)` aborts without a close handshake; the supplied code and reason are not sent. ([#914](https://github.com/openai/openai-ruby/issues/914)) ([d219f3d](https://github.com/openai/openai-ruby/commit/d219f3d0461dd96911429f1b89369eadd382f234))
|
|
45
|
+
* report terminal Realtime failures before transport cleanup ([#923](https://github.com/openai/openai-ruby/issues/923)) ([0d21d75](https://github.com/openai/openai-ruby/commit/0d21d751cd89621c6a32c0048c45ef6e5b7c1af4))
|
|
46
|
+
* retain Translation completion across callback exits ([#921](https://github.com/openai/openai-ruby/issues/921)) ([e6fb44e](https://github.com/openai/openai-ruby/commit/e6fb44e57d233e657d982eee4cf27c6a5d7977c7))
|
|
47
|
+
* share strict WebSocket model validation ([#929](https://github.com/openai/openai-ruby/issues/929)) ([51c6f0b](https://github.com/openai/openai-ruby/commit/51c6f0b73081f0f02f981d2beb4a8b129b2ec1ba))
|
|
48
|
+
* validate nested WebSocket wire field names ([#922](https://github.com/openai/openai-ruby/issues/922)) ([ae551ef](https://github.com/openai/openai-ruby/commit/ae551ef48d52d2c2f415f63c39e0c643fcfb1d75))
|
|
49
|
+
* validate typed WebSocket event constants before conversion ([#925](https://github.com/openai/openai-ruby/issues/925)) ([3ae0cd3](https://github.com/openai/openai-ruby/commit/3ae0cd3070b4fcf046f8823f4cca1b7b8829effe))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
### Chores
|
|
53
|
+
|
|
54
|
+
* **api:** clarify project API key update requirements ([#936](https://github.com/openai/openai-ruby/issues/936)) ([34606cc](https://github.com/openai/openai-ruby/commit/34606cc46087dae5a643256e9cae93966a106182))
|
|
55
|
+
* **api:** Keep SDK maintenance checks aligned ([#917](https://github.com/openai/openai-ruby/issues/917)) ([ebe5bf4](https://github.com/openai/openai-ruby/commit/ebe5bf4dd74d87acf86e30f5c7a78350ce3f8e8d))
|
|
56
|
+
* **api:** preserve audio transcription response compatibility ([#911](https://github.com/openai/openai-ruby/issues/911)) ([c93fb88](https://github.com/openai/openai-ruby/commit/c93fb88b350c80d724912351cf02d7ff51e3073a))
|
|
57
|
+
* **api:** preserve JSON Schema adapter types ([#930](https://github.com/openai/openai-ruby/issues/930)) ([f1430b8](https://github.com/openai/openai-ruby/commit/f1430b8038c7f01123fbf9f2703f6eb5b7fa8c14))
|
|
58
|
+
* **api:** preserve legacy Threads stream behavior ([#910](https://github.com/openai/openai-ruby/issues/910)) ([436887f](https://github.com/openai/openai-ruby/commit/436887fbe6bc3d0874cb75dca501ba1da0a9d9b4))
|
|
59
|
+
* **api:** preserve optional Decisions usage filters ([#909](https://github.com/openai/openai-ruby/issues/909)) ([34f61df](https://github.com/openai/openai-ruby/commit/34f61dfc23bd1ade311e09a036ef6696af37c138))
|
|
60
|
+
* **api:** preserve optional image edit safety identifiers ([#913](https://github.com/openai/openai-ruby/issues/913)) ([6bbfad1](https://github.com/openai/openai-ruby/commit/6bbfad1a6305b5efc71dbb41dce8560d2865bda2))
|
|
61
|
+
* **api:** preserve standard library loading ([#935](https://github.com/openai/openai-ruby/issues/935)) ([a742640](https://github.com/openai/openai-ruby/commit/a7426400cf499f0d88c9e88e694d86369f8d9cb5))
|
|
62
|
+
* **deps-dev:** bump the development-dependencies group with 3 updates ([#912](https://github.com/openai/openai-ruby/issues/912)) ([1227860](https://github.com/openai/openai-ruby/commit/1227860daec638117ce1f14dcd570cffc92c9647))
|
|
63
|
+
* **deps:** bump ruby/setup-ruby from 1.329.0 to 1.331.0 ([#937](https://github.com/openai/openai-ruby/issues/937)) ([8b3ab95](https://github.com/openai/openai-ruby/commit/8b3ab95e7332a7c5325b4718d4e01246c5a54054))
|
|
64
|
+
* prepare generic WebSocket binding signatures ([#941](https://github.com/openai/openai-ruby/issues/941)) ([9206c8c](https://github.com/openai/openai-ruby/commit/9206c8ceb99cc18df859d06c251fa5336c59ecdf))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
### Documentation
|
|
68
|
+
|
|
69
|
+
* clarify Realtime guide scope and Translation support ([#905](https://github.com/openai/openai-ruby/issues/905)) ([efb4004](https://github.com/openai/openai-ruby/commit/efb4004aa49a8a4374833f4b07fbd373fdebb685))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
### Refactors
|
|
73
|
+
|
|
74
|
+
* share Live codecs and use generated sideband commands ([#933](https://github.com/openai/openai-ruby/issues/933)) ([02861d0](https://github.com/openai/openai-ruby/commit/02861d0780eb18f493f881cfa9ed7cdf9a68abe3))
|
|
75
|
+
* share Translation and binding event codecs ([#932](https://github.com/openai/openai-ruby/issues/932)) ([7dc0088](https://github.com/openai/openai-ruby/commit/7dc00884ba30a54a965fc41e8b19e1840e9faec9))
|
|
76
|
+
|
|
3
77
|
## [0.106.0](https://github.com/openai/openai-ruby/compare/v0.105.0...v0.106.0) (2026-10-17)
|
|
4
78
|
|
|
5
79
|
|
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
|
|
@@ -26,6 +26,8 @@ module OpenAI
|
|
|
26
26
|
# A future server event retains its raw data, but routine diagnostics do not
|
|
27
27
|
# render potentially sensitive audio, transcript, or session fields.
|
|
28
28
|
class UnknownServerEvent
|
|
29
|
+
include OpenAI::WebSocket::Protocol
|
|
30
|
+
|
|
29
31
|
attr_reader :type, :data
|
|
30
32
|
|
|
31
33
|
def initialize(data:)
|
|
@@ -38,160 +40,33 @@ module OpenAI
|
|
|
38
40
|
def inspect = "#<#{self.class} type=#{@type.inspect}>"
|
|
39
41
|
alias to_s inspect
|
|
40
42
|
|
|
41
|
-
private def freeze_json(value)
|
|
42
|
-
pending = [value]
|
|
43
|
-
visited = {}.compare_by_identity
|
|
44
|
-
until pending.empty?
|
|
45
|
-
item = pending.pop
|
|
46
|
-
next if visited.key?(item)
|
|
47
|
-
visited[item] = true
|
|
48
|
-
case item
|
|
49
|
-
when Hash
|
|
50
|
-
item.each { |key, val| pending.push(key, val) }
|
|
51
|
-
when Array
|
|
52
|
-
pending.concat(item)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
item.freeze
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
value
|
|
59
|
-
end
|
|
60
43
|
end
|
|
61
44
|
|
|
62
45
|
# A transport-open primary connection. Startup is owned by the caller:
|
|
63
46
|
# send session.start and observe session.started before further commands.
|
|
64
|
-
class Connection < OpenAI::WebSocket::
|
|
65
|
-
include OpenAI::WebSocket::Protocol
|
|
66
|
-
include OpenAI::WebSocket::GuardedConnection
|
|
67
|
-
|
|
47
|
+
class Connection < OpenAI::WebSocket::TypedConnection
|
|
68
48
|
# @api private
|
|
69
49
|
def initialize(socket:, url:)
|
|
70
|
-
super
|
|
50
|
+
super(socket: socket, url: url, client_event: client_event_type, server_event: server_event_type)
|
|
71
51
|
@poisoned = false
|
|
72
52
|
@closed = false
|
|
73
|
-
@server_event_names = discriminator_values(server_event_type)
|
|
74
|
-
@client_event_names = discriminator_values(client_event_type)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def send_event(event)
|
|
78
|
-
send_raw(encode_client_event(event))
|
|
79
53
|
end
|
|
80
54
|
|
|
81
|
-
private
|
|
82
|
-
validate_event_tree!(event)
|
|
83
|
-
payload = OpenAI::Internal::Type::Converter.dump(client_event_type, event)
|
|
84
|
-
type = payload[:type] || payload["type"] if payload.is_a?(Hash)
|
|
85
|
-
raise ArgumentError unless (type.is_a?(String) || type.is_a?(Symbol)) && @client_event_names.key?(type.to_s)
|
|
86
|
-
|
|
87
|
-
coerced = coerce_event(client_event_type, payload, outbound: true)
|
|
88
|
-
serialized = OpenAI::Internal::Type::Converter.dump(client_event_type, coerced)
|
|
89
|
-
validate_event_tree!(serialized, json_only: true)
|
|
90
|
-
JSON.generate(serialized, max_nesting: false)
|
|
91
|
-
rescue StandardError, SystemStackError
|
|
92
|
-
raise ArgumentError, "Invalid Live client event.", cause: nil
|
|
93
|
-
end
|
|
55
|
+
private :parse_event
|
|
94
56
|
|
|
95
|
-
private def
|
|
96
|
-
parsed = JSON.parse(data, symbolize_names: true, max_nesting: false)
|
|
97
|
-
type = event_type(parsed, message: "Live server event must be an object with a string type")
|
|
98
|
-
unless @server_event_names.key?(type.to_s)
|
|
99
|
-
return OpenAI::Live::UnknownServerEvent.new(data: parsed)
|
|
100
|
-
end
|
|
57
|
+
private def coerce_client_event(payload) = coerce_event(@client_event, payload, outbound: true)
|
|
101
58
|
|
|
102
|
-
|
|
59
|
+
private def coerce(type, payload)
|
|
60
|
+
event = coerce_event(type, payload)
|
|
103
61
|
validate_event_tree!(event)
|
|
104
62
|
event
|
|
105
|
-
rescue StandardError, SystemStackError
|
|
106
|
-
raise OpenAI::Errors::LiveProtocolError.new, cause: nil
|
|
107
63
|
end
|
|
108
64
|
|
|
109
|
-
private def
|
|
110
|
-
state = OpenAI::Internal::Type::Converter.new_coerce_state(request_only: outbound)
|
|
111
|
-
event = OpenAI::Internal::Type::Converter.coerce(union, payload, state: state)
|
|
112
|
-
raise ArgumentError if state[:error] || !state.fetch(:exactness).fetch(:no).zero?
|
|
113
|
-
pending = [event]
|
|
114
|
-
until pending.empty?
|
|
115
|
-
value = pending.pop
|
|
116
|
-
case value
|
|
117
|
-
when OpenAI::Internal::Type::BaseModel
|
|
118
|
-
value.class.fields.each do |name, field|
|
|
119
|
-
if field.fetch(:required) &&
|
|
120
|
-
field.fetch(:mode) != (outbound ? :coerce : :dump) &&
|
|
121
|
-
(!outbound || field.fetch(:const) == OpenAI::Internal::OMIT) &&
|
|
122
|
-
!value.to_h.key?(name)
|
|
123
|
-
raise ArgumentError
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
pending.concat(value.to_h.values)
|
|
128
|
-
when Hash
|
|
129
|
-
pending.concat(value.values)
|
|
130
|
-
when Array
|
|
131
|
-
pending.concat(value)
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
event
|
|
136
|
-
end
|
|
65
|
+
private def client_event_error_message = "Invalid Live client event."
|
|
137
66
|
|
|
138
|
-
private def
|
|
139
|
-
ancestors = {}.compare_by_identity
|
|
140
|
-
pending = [[event, false]]
|
|
141
|
-
until pending.empty?
|
|
142
|
-
value, exiting = pending.pop
|
|
143
|
-
if exiting
|
|
144
|
-
ancestors.delete(value)
|
|
145
|
-
next
|
|
146
|
-
end
|
|
67
|
+
private def unknown_server_event(payload) = OpenAI::Live::UnknownServerEvent.new(data: payload)
|
|
147
68
|
|
|
148
|
-
|
|
149
|
-
when OpenAI::Internal::Type::BaseModel
|
|
150
|
-
raise ArgumentError if json_only
|
|
151
|
-
data = value.to_h
|
|
152
|
-
keys = {}
|
|
153
|
-
data.each do |key, val|
|
|
154
|
-
name = key.is_a?(String) ? key.to_sym : key
|
|
155
|
-
field = value.class.known_fields[name]
|
|
156
|
-
serialized_name = field ? field.fetch(:api_name).to_s : name.to_s
|
|
157
|
-
raise ArgumentError if keys.key?(serialized_name)
|
|
158
|
-
keys[serialized_name] = true
|
|
159
|
-
next unless field
|
|
160
|
-
const = field.fetch(:const)
|
|
161
|
-
next if const == OpenAI::Internal::OMIT
|
|
162
|
-
if const.is_a?(Symbol)
|
|
163
|
-
raise ArgumentError unless (val.is_a?(String) || val.is_a?(Symbol)) && val.to_s == const.to_s
|
|
164
|
-
elsif val != const
|
|
165
|
-
raise ArgumentError
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
[data]
|
|
170
|
-
when Hash
|
|
171
|
-
keys = {}
|
|
172
|
-
value.each_key do |key|
|
|
173
|
-
raise ArgumentError unless key.is_a?(String) || key.is_a?(Symbol)
|
|
174
|
-
name = key.to_s
|
|
175
|
-
raise ArgumentError if keys.key?(name)
|
|
176
|
-
keys[name] = true
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
value.values
|
|
180
|
-
when Array
|
|
181
|
-
value
|
|
182
|
-
when NilClass, TrueClass, FalseClass, String, Integer, Float, Symbol
|
|
183
|
-
next
|
|
184
|
-
else
|
|
185
|
-
raise ArgumentError if json_only
|
|
186
|
-
next
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
raise ArgumentError if ancestors.key?(value)
|
|
190
|
-
ancestors[value] = true
|
|
191
|
-
pending << [value, true]
|
|
192
|
-
children.each { |child| pending << [child, false] }
|
|
193
|
-
end
|
|
194
|
-
end
|
|
69
|
+
private def protocol_error = OpenAI::Errors::LiveProtocolError.new
|
|
195
70
|
|
|
196
71
|
private def transport_error = OpenAI::Errors::LiveConnectionError.new(url: @url)
|
|
197
72
|
private def failed_read_message = "Cannot read from a failed Live WebSocket."
|
|
@@ -206,11 +81,7 @@ module OpenAI
|
|
|
206
81
|
|
|
207
82
|
# Attached to an existing session; no new session.start handshake is sent.
|
|
208
83
|
class SidebandConnection < Connection
|
|
209
|
-
|
|
210
|
-
def initialize(socket:, url:)
|
|
211
|
-
super
|
|
212
|
-
@client_event_names = @client_event_names.except("session.start", "session.input_audio.append")
|
|
213
|
-
end
|
|
84
|
+
private def client_event_type = OpenAI::Live::SidebandClientEvent
|
|
214
85
|
end
|
|
215
86
|
|
|
216
87
|
# Caller-driven startup using overrides for an eligible stored session.
|