openai 0.95.0 → 0.96.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 +44 -0
- data/README.md +5 -2
- data/docs/architecture/security-model.md +2 -2
- data/examples/audio/record_and_play.rb +1 -1
- data/lib/openai/helpers/live/transcript_grouper.rb +264 -0
- data/lib/openai/helpers/live/transcript_grouping.rb +309 -0
- data/lib/openai/helpers/live/websocket.rb +454 -0
- data/lib/openai/helpers/realtime/translation.rb +440 -0
- data/lib/openai/helpers/realtime.rb +1 -0
- data/lib/openai/helpers/responses_websocket/incremental.rb +246 -0
- data/lib/openai/helpers/responses_websocket/transports/async_websocket.rb +183 -0
- data/lib/openai/helpers/responses_websocket.rb +1 -0
- data/lib/openai/helpers/streaming/response_stream.rb +19 -11
- data/lib/openai/helpers/structured_output/chat_completion_parser.rb +0 -2
- data/lib/openai/helpers/structured_output/response_parser.rb +1 -2
- data/lib/openai/helpers/structured_output/union_of.rb +0 -2
- data/lib/openai/helpers/websocket/async_websocket_transport.rb +11 -6
- data/lib/openai/helpers/websocket/connection_manager.rb +9 -0
- data/lib/openai/internal/read_io_adapter.rb +0 -2
- data/lib/openai/internal/stream.rb +0 -2
- data/lib/openai/internal/type/base_model.rb +4 -6
- data/lib/openai/internal/type/converter.rb +5 -3
- data/lib/openai/internal/type/enum.rb +0 -2
- data/lib/openai/internal/type/union.rb +0 -6
- data/lib/openai/internal/type/unknown.rb +34 -6
- data/lib/openai/internal/util.rb +0 -4
- data/lib/openai/models/audio/speech_create_params.rb +9 -0
- data/lib/openai/models/audio/transcription_create_response.rb +4 -1
- data/lib/openai/models/audio/translation_create_response.rb +4 -1
- data/lib/openai/models/beta/agent.rb +1 -0
- data/lib/openai/models/beta/agent_create_params.rb +3 -0
- data/lib/openai/models/beta/agent_session.rb +1 -0
- data/lib/openai/models/beta/agent_update_params.rb +3 -0
- data/lib/openai/models/beta/agents/environments/template_create_params.rb +3 -3
- data/lib/openai/models/beta/agents/environments/template_update_params.rb +6 -6
- data/lib/openai/models/beta/agents/session_create_params.rb +3 -0
- data/lib/openai/models/beta/agents/session_update_params.rb +3 -0
- data/lib/openai/models/beta/agents/vaults/credential.rb +10 -1
- data/lib/openai/models/beta/agents/vaults/credential_create_params.rb +12 -1
- data/lib/openai/models/beta/agents/vaults/credential_update_params.rb +14 -3
- data/lib/openai/models/beta/beta_response.rb +38 -1
- data/lib/openai/models/beta/beta_responses_client_event.rb +73 -1
- data/lib/openai/models/beta/beta_web_search_preview_tool.rb +9 -3
- data/lib/openai/models/beta/beta_web_search_tool.rb +9 -3
- data/lib/openai/models/beta/environment_param.rb +3 -3
- data/lib/openai/models/beta/response_compact_params.rb +3 -0
- data/lib/openai/models/beta/response_create_params.rb +68 -1
- data/lib/openai/models/beta/session_turn_error.rb +3 -0
- data/lib/openai/models/chat_model.rb +1 -0
- data/lib/openai/models/responses/response.rb +35 -1
- data/lib/openai/models/responses/response_compact_params.rb +3 -0
- data/lib/openai/models/responses/response_create_params.rb +65 -1
- data/lib/openai/models/responses/responses_client_event.rb +70 -1
- data/lib/openai/models/responses/web_search_preview_tool.rb +9 -3
- data/lib/openai/models/responses/web_search_tool.rb +9 -3
- data/lib/openai/net_http_client.rb +0 -2
- data/lib/openai/resources/audio/transcriptions.rb +5 -3
- data/lib/openai/resources/audio/translations.rb +1 -1
- data/lib/openai/resources/beta/agents/environments/templates.rb +3 -3
- data/lib/openai/resources/beta/agents/vaults/credentials.rb +12 -4
- data/lib/openai/resources/beta/responses.rb +8 -2
- data/lib/openai/resources/responses.rb +8 -1
- data/lib/openai/version.rb +1 -1
- data/lib/openai.rb +1 -0
- data/rbi/openai/helpers/live/transcript_grouper.rbi +67 -0
- data/rbi/openai/helpers/live/websocket.rbi +299 -0
- data/rbi/openai/helpers/realtime/translation.rbi +137 -0
- data/rbi/openai/helpers/responses_websocket/incremental.rbi +32 -0
- data/rbi/openai/helpers/responses_websocket/transports/async_websocket.rbi +24 -0
- data/rbi/openai/helpers/streaming/response_stream.rbi +5 -0
- data/rbi/openai/internal/type/converter.rbi +3 -2
- data/rbi/openai/models/audio/speech_create_params.rbi +3 -0
- data/rbi/openai/models/audio/transcription_create_response.rbi +2 -1
- data/rbi/openai/models/audio/translation_create_response.rbi +2 -1
- data/rbi/openai/models/beta/agent.rbi +1 -0
- data/rbi/openai/models/beta/agent_create_params.rbi +3 -0
- data/rbi/openai/models/beta/agent_session.rbi +1 -0
- data/rbi/openai/models/beta/agent_update_params.rbi +3 -0
- data/rbi/openai/models/beta/agents/environments/template_create_params.rbi +3 -3
- data/rbi/openai/models/beta/agents/environments/template_update_params.rbi +6 -6
- data/rbi/openai/models/beta/agents/session_create_params.rbi +3 -0
- data/rbi/openai/models/beta/agents/session_update_params.rbi +3 -0
- data/rbi/openai/models/beta/agents/vaults/credential.rbi +10 -0
- data/rbi/openai/models/beta/agents/vaults/credential_create_params.rbi +15 -0
- data/rbi/openai/models/beta/agents/vaults/credential_update_params.rbi +35 -6
- data/rbi/openai/models/beta/beta_response.rbi +64 -0
- data/rbi/openai/models/beta/beta_responses_client_event.rbi +128 -0
- data/rbi/openai/models/beta/beta_web_search_preview_tool.rbi +9 -3
- data/rbi/openai/models/beta/beta_web_search_tool.rbi +9 -3
- data/rbi/openai/models/beta/environment_param.rbi +3 -3
- data/rbi/openai/models/beta/response_compact_params.rbi +1 -0
- data/rbi/openai/models/beta/response_create_params.rbi +100 -0
- data/rbi/openai/models/beta/session_turn_error.rbi +6 -0
- data/rbi/openai/models/chat_model.rbi +1 -0
- data/rbi/openai/models/responses/response.rbi +63 -0
- data/rbi/openai/models/responses/response_compact_params.rbi +1 -0
- data/rbi/openai/models/responses/response_create_params.rbi +106 -0
- data/rbi/openai/models/responses/responses_client_event.rbi +125 -0
- data/rbi/openai/models/responses/web_search_preview_tool.rbi +9 -3
- data/rbi/openai/models/responses/web_search_tool.rbi +9 -3
- data/rbi/openai/resources/audio/transcriptions.rbi +4 -2
- data/rbi/openai/resources/beta/agents/environments/templates.rbi +3 -3
- data/rbi/openai/resources/beta/agents/vaults/credentials.rbi +11 -3
- data/rbi/openai/resources/beta/responses.rbi +6 -0
- data/rbi/openai/resources/responses.rbi +11 -0
- data/responses-websocket.md +55 -0
- data/sig/openai/helpers/live/transcript_grouper.rbs +42 -0
- data/sig/openai/helpers/live/websocket.rbs +152 -0
- data/sig/openai/helpers/realtime/translation.rbs +81 -0
- data/sig/openai/helpers/responses_websocket/incremental.rbs +14 -0
- data/sig/openai/helpers/responses_websocket/transports/async_websocket.rbs +21 -0
- data/sig/openai/helpers/streaming/response_stream.rbs +9 -0
- data/sig/openai/internal/type/converter.rbs +3 -1
- data/sig/openai/models/audio/speech_create_params.rbs +6 -0
- data/sig/openai/models/audio/transcription_create_response.rbs +1 -0
- data/sig/openai/models/audio/translation_create_response.rbs +3 -1
- data/sig/openai/models/beta/agent.rbs +3 -1
- data/sig/openai/models/beta/agent_create_params.rbs +5 -1
- data/sig/openai/models/beta/agent_session.rbs +3 -1
- data/sig/openai/models/beta/agent_update_params.rbs +5 -1
- data/sig/openai/models/beta/agents/environments/template_create_params.rbs +2 -2
- data/sig/openai/models/beta/agents/environments/template_update_params.rbs +4 -4
- data/sig/openai/models/beta/agents/session_create_params.rbs +5 -1
- data/sig/openai/models/beta/agents/session_update_params.rbs +5 -1
- data/sig/openai/models/beta/agents/vaults/credential.rbs +8 -0
- data/sig/openai/models/beta/agents/vaults/credential_create_params.rbs +14 -0
- data/sig/openai/models/beta/agents/vaults/credential_update_params.rbs +23 -4
- data/sig/openai/models/beta/beta_response.rbs +37 -0
- data/sig/openai/models/beta/beta_responses_client_event.rbs +79 -0
- data/sig/openai/models/beta/beta_web_search_preview_tool.rbs +6 -2
- data/sig/openai/models/beta/beta_web_search_tool.rbs +6 -2
- data/sig/openai/models/beta/environment_param.rbs +2 -2
- data/sig/openai/models/beta/response_compact_params.rbs +2 -0
- data/sig/openai/models/beta/response_create_params.rbs +79 -0
- data/sig/openai/models/beta/session_turn_error.rbs +4 -0
- data/sig/openai/models/chat_model.rbs +2 -0
- data/sig/openai/models/responses/response.rbs +35 -0
- data/sig/openai/models/responses/response_compact_params.rbs +2 -0
- data/sig/openai/models/responses/response_create_params.rbs +77 -0
- data/sig/openai/models/responses/responses_client_event.rbs +77 -0
- data/sig/openai/models/responses/web_search_preview_tool.rbs +6 -2
- data/sig/openai/models/responses/web_search_tool.rbs +6 -2
- data/sig/openai/resources/beta/agents/vaults/credentials.rbs +3 -1
- data/sig/openai/resources/beta/responses.rbs +2 -0
- data/sig/openai/resources/responses.rbs +4 -0
- metadata +14 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 45b725bdf2e7a6b6837d634f0a91df36e77e4051d3a44019742cd9176ecf53ec
|
|
4
|
+
data.tar.gz: d42beb5a3f77a5cb8ef597fa8ce4bb80c6a5485252becedbd67ec4e65ef5f674
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ac89793e8ba7b821b5d98dbff7b8bf366d6ab251c06fdc2f23422347de291443267343bb5fac7c4c17e4fee1e357873681fdbd761aa16db0d6d59cca7a85c1e
|
|
7
|
+
data.tar.gz: 284f3124b3d366c6bd1ab7bc9955ed29ce4e5a6da39e08320fcc575847b7bf95f0246208cdb06042c049ef3226c2e3bae6f8551aca343f724c5ced65532b456a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.96.0](https://github.com/openai/openai-ruby/compare/v0.95.0...v0.96.0) (2026-09-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **api:** add Agents credential and session options ([#766](https://github.com/openai/openai-ruby/issues/766)) ([27acf38](https://github.com/openai/openai-ruby/commit/27acf382abf7e94c051d35ca3d48870064ff9d27))
|
|
9
|
+
* **api:** add Cyber access programs to Responses ([#757](https://github.com/openai/openai-ruby/issues/757)) ([f484e4e](https://github.com/openai/openai-ruby/commit/f484e4ee112023c2aadc71f9af549e3adf130bc1))
|
|
10
|
+
* **api:** add GPT-6.1 Sol model identifier ([#781](https://github.com/openai/openai-ruby/issues/781)) ([a97c14f](https://github.com/openai/openai-ruby/commit/a97c14f89620aaccda51d593b00e7c7a16507eb6))
|
|
11
|
+
* **live:** add optional transcript grouping over typed events ([#769](https://github.com/openai/openai-ruby/issues/769)) ([d6f2548](https://github.com/openai/openai-ruby/commit/d6f254810754f6310ead16928069a929c8a1a675))
|
|
12
|
+
* **live:** add sideband attachment and stored fork WebSockets ([#771](https://github.com/openai/openai-ruby/issues/771)) ([7c6cf76](https://github.com/openai/openai-ruby/commit/7c6cf76e5c381707a1c2c6b4c8a8f6e347ee2ddb))
|
|
13
|
+
* **live:** support caller-started primary WebSockets ([#770](https://github.com/openai/openai-ruby/issues/770)) ([dc3566d](https://github.com/openai/openai-ruby/commit/dc3566ddd9bfff7dd47a0214d8596dc95207284d))
|
|
14
|
+
* **realtime:** add bounded finish for Translation WebSocket consumers ([#778](https://github.com/openai/openai-ruby/issues/778)) ([a92b422](https://github.com/openai/openai-ruby/commit/a92b422402da28c0f96b9fbd4cd635ee693211c9))
|
|
15
|
+
* **realtime:** add typed Translation WebSocket connection ([#772](https://github.com/openai/openai-ruby/issues/772)) ([b3706d8](https://github.com/openai/openai-ruby/commit/b3706d84fbb68e700bb726559fba0da0393de567))
|
|
16
|
+
* **responses:** add opt-in WebSocket incremental output ([#768](https://github.com/openai/openai-ruby/issues/768)) ([e190303](https://github.com/openai/openai-ruby/commit/e1903035d709a6b8aa40c0049ea8305c63eccc61))
|
|
17
|
+
* **responses:** allow bounded WebSocket messages before allocation ([#775](https://github.com/openai/openai-ruby/issues/775)) ([bef7b8b](https://github.com/openai/openai-ruby/commit/bef7b8bc66d5feb42e8342fc4b3ef708fde129c5))
|
|
18
|
+
* **responses:** bound compressed WebSocket wire and decoded sizes ([#776](https://github.com/openai/openai-ruby/issues/776)) ([0cc6b15](https://github.com/openai/openai-ruby/commit/0cc6b15a6049b43145ea806b129e1708c8836800))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
|
|
23
|
+
* **websocket:** include default ports in proxy tunnels ([#780](https://github.com/openai/openai-ruby/issues/780)) ([c496e6b](https://github.com/openai/openai-ruby/commit/c496e6b301834ed5aea18ba09c514411eabf5776))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Chores
|
|
27
|
+
|
|
28
|
+
* **api:** clarify approximate web search location defaults ([#755](https://github.com/openai/openai-ruby/issues/755)) ([f83af03](https://github.com/openai/openai-ruby/commit/f83af03a667a7045dde39d2a9ca328432e3d109b))
|
|
29
|
+
* **api:** clarify documented API error responses ([#765](https://github.com/openai/openai-ruby/issues/765)) ([6f786c6](https://github.com/openai/openai-ruby/commit/6f786c6a752a0a2773ecf7c1b0b4490a94104c15))
|
|
30
|
+
* **api:** clarify Realtime modality array definitions ([#756](https://github.com/openai/openai-ruby/issues/756)) ([9e18acf](https://github.com/openai/openai-ruby/commit/9e18acfc93ba6fbedaae37bb89956b1a4cc24b49))
|
|
31
|
+
* **api:** correct fine-tuning bounds and Realtime response reference ([#752](https://github.com/openai/openai-ruby/issues/752)) ([4c1cd58](https://github.com/openai/openai-ruby/commit/4c1cd583ee2b79b0355fcc30f3b340c68dcfc337))
|
|
32
|
+
* **api:** document batch error responses ([#760](https://github.com/openai/openai-ruby/issues/760)) ([38fec96](https://github.com/openai/openai-ruby/commit/38fec96d1c3bb77616e741e3c0d5d11b1f385b46))
|
|
33
|
+
* **api:** document files and uploads error responses ([#759](https://github.com/openai/openai-ruby/issues/759)) ([6cdd7b1](https://github.com/openai/openai-ruby/commit/6cdd7b1785989cdaed5e81fb956cac2ccadd2361))
|
|
34
|
+
* **api:** document fine-tuning and model errors ([#764](https://github.com/openai/openai-ruby/issues/764)) ([4b3b043](https://github.com/openai/openai-ruby/commit/4b3b043176413f6c70309290bf8f4129a8961c16))
|
|
35
|
+
* **api:** document Responses not-found errors ([#758](https://github.com/openai/openai-ruby/issues/758)) ([1d556e2](https://github.com/openai/openai-ruby/commit/1d556e20aef0e30bd6990dda1b7d9059b1d0817e))
|
|
36
|
+
* **api:** document stored chat completion errors ([#763](https://github.com/openai/openai-ruby/issues/763)) ([ddd1a68](https://github.com/openai/openai-ruby/commit/ddd1a68179ad1ec90af55f39f71310a59f92ddcf))
|
|
37
|
+
* **deps-dev:** bump sorbet-runtime from 0.6.13499 to 0.6.13506 in the development-dependencies group ([#773](https://github.com/openai/openai-ruby/issues/773)) ([be9c528](https://github.com/openai/openai-ruby/commit/be9c5289d5d7eea7c71e78be640c192f81c6544d))
|
|
38
|
+
* **deps:** bump ruby/setup-ruby from 1.324.0 to 1.326.0 ([#749](https://github.com/openai/openai-ruby/issues/749)) ([5aae05c](https://github.com/openai/openai-ruby/commit/5aae05c4b42f9d509bdf084282a7e33ef8758adc))
|
|
39
|
+
* **deps:** bump ruby/setup-ruby from 1.326.0 to 1.327.0 ([#762](https://github.com/openai/openai-ruby/issues/762)) ([3921459](https://github.com/openai/openai-ruby/commit/39214594edf3c07ed2ce71c603946462735fa6ad))
|
|
40
|
+
* **deps:** bump the codeql group with 2 updates ([#761](https://github.com/openai/openai-ruby/issues/761)) ([0185fa1](https://github.com/openai/openai-ruby/commit/0185fa1c637f1dbc9d3b3fa85c4c46a199702188))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
### Documentation
|
|
44
|
+
|
|
45
|
+
* clarify collaborator-only pull request policy ([#750](https://github.com/openai/openai-ruby/issues/750)) ([c704778](https://github.com/openai/openai-ruby/commit/c7047782ff2ae38a859a08ef199e67a79c16b9fe))
|
|
46
|
+
|
|
3
47
|
## [0.95.0](https://github.com/openai/openai-ruby/compare/v0.94.0...v0.95.0) (2026-09-23)
|
|
4
48
|
|
|
5
49
|
|
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.96.0"
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
<!-- x-release-please-end -->
|
|
@@ -1293,4 +1293,7 @@ This line will not receive separate maintenance.
|
|
|
1293
1293
|
|
|
1294
1294
|
## Contributing
|
|
1295
1295
|
|
|
1296
|
-
|
|
1296
|
+
Please share bug reports and feature requests through [GitHub issues](https://github.com/openai/openai-ruby/issues).
|
|
1297
|
+
Pull requests are limited to repository collaborators; we do not accept pull requests from non-collaborators.
|
|
1298
|
+
See [CONTRIBUTING.md](https://github.com/openai/openai-ruby/blob/main/CONTRIBUTING.md) for the contribution policy and development guide.
|
|
1299
|
+
For security vulnerabilities, follow [SECURITY.md](https://github.com/openai/openai-ruby/blob/main/SECURITY.md).
|
|
@@ -162,7 +162,7 @@ Important boundaries:
|
|
|
162
162
|
`lib/openai/internal/stream.rb:35-40`,
|
|
163
163
|
`lib/openai/helpers/realtime/errors.rb:26-70`,
|
|
164
164
|
`lib/openai/http_client.rb:46-79`,
|
|
165
|
-
`CONTRIBUTING.md:
|
|
165
|
+
`CONTRIBUTING.md:61-69`).
|
|
166
166
|
- Candidate PR code reaching sensitive live/release credentials or repository
|
|
167
167
|
write/publishing authority is a genuine boundary
|
|
168
168
|
(`.github/workflows/live-smoke.yml:17-92`,
|
|
@@ -268,7 +268,7 @@ The following are hypotheses and review guidance, not confirmed findings.
|
|
|
268
268
|
| Medium | Malformed or adversarial API/SSE/WebSocket data causes sensitive leakage, type confusion, or availability failure | Victim consumes untrusted remote data | Data exposure, incorrect application behavior, or DoS | JSON parsing, typed coercion, protocol errors, safe unknown events | Keep parsers data-only, incremental where applicable, and redact errors/logs | `lib/openai/internal/stream.rb:21-60`, `lib/openai/helpers/realtime/connection.rb:61-84` |
|
|
269
269
|
| Medium | Multipart filename, content type, or field name reaches headers without normalization | Caller-supplied path or header parameter crosses multipart serialization | Absolute local path disclosure or multipart header injection | Media-type validation, basename reduction for `FilePart` and raw path/IO values, quote/backslash escaping, CR/LF removal | Preserve basename and header-parameter controls before upload | `lib/openai/file_part.rb:33-43`, `lib/openai/file_part.rb:110-119`, `lib/openai/internal/util.rb:533-565` |
|
|
270
270
|
| Medium | Forged webhook, or replay of a captured valid webhook, becomes an accepted typed event | For forgery, attacker lacks the webhook secret; for replay, attacker can resend a valid signed payload within the tolerance window | Application accepts an unauthentic or repeated event | Required headers, HMAC, and timing-safe comparison reject forgery; timestamp validation bounds replay age but does not deduplicate `webhook-id` | Verify before parsing; callers that need replay prevention must deduplicate `webhook-id` | `lib/openai/resources/webhooks.rb:15-119` |
|
|
271
|
-
| Medium | Sensitive runtime or live-example data leaks through diagnostics, exceptions, retained metadata, or artifacts | Untrusted data reaches logger/error/object serialization or a live-example report | Credential, prompt, response, file, header, or call-identifier disclosure | Redacted headers/queries; unsafe body categories omitted; derived HTTP errors are bounded/sanitized when no top-level scalar or explicit message is supplied; top-level scalar and explicit SSE messages remain remote data; Realtime URL call-ID redaction; retained bodies omitted from inspect/YAML/Marshal; Examples E2E reports are allowlisted and tested for sensitive output | Preserve redaction, bounded derived errors, metadata serialization controls, and allowlisted artifacts; treat preserved remote messages as remote data | `lib/openai/internal/logging.rb:299-343`, `lib/openai/errors.rb:259-409`, `lib/openai/internal/stream.rb:35-40`, `lib/openai/helpers/realtime/errors.rb:26-70`, `lib/openai/http_client.rb:46-79`, `.github/workflows/examples-e2e.yml:29-43`, `test/scripts/examples_e2e_test.rb:202-234`, `test/scripts/examples_e2e_test.rb:306-327`, `CONTRIBUTING.md:
|
|
271
|
+
| Medium | Sensitive runtime or live-example data leaks through diagnostics, exceptions, retained metadata, or artifacts | Untrusted data reaches logger/error/object serialization or a live-example report | Credential, prompt, response, file, header, or call-identifier disclosure | Redacted headers/queries; unsafe body categories omitted; derived HTTP errors are bounded/sanitized when no top-level scalar or explicit message is supplied; top-level scalar and explicit SSE messages remain remote data; Realtime URL call-ID redaction; retained bodies omitted from inspect/YAML/Marshal; Examples E2E reports are allowlisted and tested for sensitive output | Preserve redaction, bounded derived errors, metadata serialization controls, and allowlisted artifacts; treat preserved remote messages as remote data | `lib/openai/internal/logging.rb:299-343`, `lib/openai/errors.rb:259-409`, `lib/openai/internal/stream.rb:35-40`, `lib/openai/helpers/realtime/errors.rb:26-70`, `lib/openai/http_client.rb:46-79`, `.github/workflows/examples-e2e.yml:29-43`, `test/scripts/examples_e2e_test.rb:202-234`, `test/scripts/examples_e2e_test.rb:306-327`, `CONTRIBUTING.md:61-69` |
|
|
272
272
|
| Not a finding by itself | Contributor changes a tracked test/example/fixture/build script and ordinary CI runs it | Contributor already controls candidate checkout code | No new authority | Read-only ordinary CI without referenced secrets or write authority | Require a separate privilege or boundary violation before reporting | `.github/workflows/ci.yml:18-32`, `.github/workflows/ci-checks.yml:100-120` |
|
|
273
273
|
|
|
274
274
|
## 4. Severity Calibration
|
|
@@ -14,6 +14,6 @@ VOICE = "marin"
|
|
|
14
14
|
client = OpenAI::Client.new
|
|
15
15
|
clip = OpenAI::LocalAudio.record(duration: 5, timeout: 10)
|
|
16
16
|
transcription = client.audio.transcriptions.create(model: TRANSCRIPTION_MODEL, file: clip)
|
|
17
|
-
puts(transcription.text)
|
|
17
|
+
puts(transcription.is_a?(StringIO) ? transcription.read : transcription.text)
|
|
18
18
|
speech = client.audio.speech.create(model: SPEECH_MODEL, voice: VOICE, input: "Recording complete.")
|
|
19
19
|
OpenAI::LocalAudio.play(speech, timeout: 30)
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../openai"
|
|
4
|
+
|
|
5
|
+
module OpenAI
|
|
6
|
+
module Live
|
|
7
|
+
# Groups public Live transcript deltas into immutable display snapshots.
|
|
8
|
+
# This is a speaker/acknowledgment heuristic, not VAD or lossless history.
|
|
9
|
+
# Create one per session and close it on transport loss or teardown.
|
|
10
|
+
class TranscriptGrouper
|
|
11
|
+
# IDs belong to this local display projection, not server turns or items.
|
|
12
|
+
# Each update contains the complete text; replace the displayed text.
|
|
13
|
+
Segment = Data.define(:id, :previous_id, :speaker, :text, :start_ms, :end_ms)
|
|
14
|
+
ClosedSegment = Data.define(:segment, :reason)
|
|
15
|
+
|
|
16
|
+
# @api private
|
|
17
|
+
Options = Data.define(
|
|
18
|
+
:min_turn_separation_ms,
|
|
19
|
+
:assistant_silence_ms,
|
|
20
|
+
:backchannel_max_duration_ms,
|
|
21
|
+
:backchannel_isolation_ms,
|
|
22
|
+
:additional_acknowledgments
|
|
23
|
+
)
|
|
24
|
+
# @api private
|
|
25
|
+
Fragment = Data.define(:speaker, :text, :start_ms, :end_ms, :received_at)
|
|
26
|
+
# @api private
|
|
27
|
+
Update = Data.define(:segment, :reason)
|
|
28
|
+
private_constant :Options, :Fragment, :Update
|
|
29
|
+
|
|
30
|
+
# Timing values are milliseconds, between 0 and 2147483647.
|
|
31
|
+
# Set backchannel_max_duration_ms to zero to disable suppression.
|
|
32
|
+
# Callbacks are serialized outside the state lock and may reenter push/close.
|
|
33
|
+
# They run on the caller or the owned timer thread; exceptions propagate.
|
|
34
|
+
def initialize(
|
|
35
|
+
on_segment_updated: nil,
|
|
36
|
+
on_segment_closed: nil,
|
|
37
|
+
min_turn_separation_ms: 500,
|
|
38
|
+
assistant_silence_ms: 2000,
|
|
39
|
+
backchannel_max_duration_ms: 1000,
|
|
40
|
+
backchannel_isolation_ms: 2000,
|
|
41
|
+
additional_acknowledgments: []
|
|
42
|
+
)
|
|
43
|
+
timings = [min_turn_separation_ms, assistant_silence_ms, backchannel_max_duration_ms, backchannel_isolation_ms]
|
|
44
|
+
unless timings.all? { |value|
|
|
45
|
+
(value.is_a?(Integer) || value.is_a?(Float)) && value.between?(0, 2_147_483_647)
|
|
46
|
+
}
|
|
47
|
+
raise ArgumentError, "Timing must be between 0 and 2147483647 ms"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
unless additional_acknowledgments.is_a?(Array) &&
|
|
51
|
+
additional_acknowledgments.all? { |value| value.is_a?(String) }
|
|
52
|
+
raise ArgumentError, "additional_acknowledgments must be an array of strings"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
unless [on_segment_updated, on_segment_closed].all? { |value| value.nil? || value.respond_to?(:call) }
|
|
56
|
+
raise ArgumentError, "Transcript callbacks must respond to call"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
options = Options.new(*timings, additional_acknowledgments)
|
|
60
|
+
@grouping = Grouping.new(options, "segment_#{SecureRandom.hex(8)}")
|
|
61
|
+
@on_segment_updated = on_segment_updated
|
|
62
|
+
@on_segment_closed = on_segment_closed
|
|
63
|
+
@lock = Mutex.new
|
|
64
|
+
@wake = ConditionVariable.new
|
|
65
|
+
@seen_ids = {}
|
|
66
|
+
@last_start_ms = {}
|
|
67
|
+
@pending = []
|
|
68
|
+
@updates = []
|
|
69
|
+
@closed = false
|
|
70
|
+
@dispatching = false
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Consume a typed Live event. Duplicate IDs, empty text and other events
|
|
74
|
+
# are ignored. Invalid transcript fields and use after close raise.
|
|
75
|
+
# Original events and their strings remain owned by the caller.
|
|
76
|
+
# @return [void]
|
|
77
|
+
def push(event)
|
|
78
|
+
should_drain = @lock.synchronize do
|
|
79
|
+
raise RuntimeError, "Cannot push events after closing the transcript grouper" if @closed
|
|
80
|
+
|
|
81
|
+
if event.is_a?(SessionClosedEvent)
|
|
82
|
+
enqueue(finish(:session_closed))
|
|
83
|
+
else
|
|
84
|
+
speaker = case event
|
|
85
|
+
when InputTranscriptDeltaEvent
|
|
86
|
+
:user
|
|
87
|
+
when OutputTranscriptDeltaEvent
|
|
88
|
+
:assistant
|
|
89
|
+
else
|
|
90
|
+
return
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
fragment = normalize(event, speaker)
|
|
94
|
+
return if @seen_ids.key?(event.event_id)
|
|
95
|
+
|
|
96
|
+
@seen_ids[event.event_id.dup.freeze] = true
|
|
97
|
+
return if fragment.text.empty?
|
|
98
|
+
|
|
99
|
+
emitted = []
|
|
100
|
+
first = @pending.first
|
|
101
|
+
if first && first.start_ms == fragment.start_ms && first.end_ms == fragment.end_ms
|
|
102
|
+
@pending << fragment
|
|
103
|
+
emitted.concat(flush_pending) if @pending.any? { |part| part.speaker != speaker }
|
|
104
|
+
else
|
|
105
|
+
emitted.concat(flush_pending)
|
|
106
|
+
if @grouping.speaker == speaker
|
|
107
|
+
emitted.concat(commit([fragment]))
|
|
108
|
+
else
|
|
109
|
+
@pending << fragment
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
start_timer
|
|
114
|
+
@wake.signal
|
|
115
|
+
enqueue(emitted)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
drain if should_drain
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Flush/finalize once and cancel the owned timer. Never closes a transport.
|
|
124
|
+
# A callback already running can finish after close returns.
|
|
125
|
+
# @return [void]
|
|
126
|
+
def close
|
|
127
|
+
should_drain = @lock.synchronize { enqueue(@closed ? [] : finish(:manual)) }
|
|
128
|
+
drain if should_drain
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def normalize(event, speaker)
|
|
135
|
+
unless event.event_id.is_a?(String) &&
|
|
136
|
+
!event.event_id.empty? &&
|
|
137
|
+
event.delta.is_a?(String) &&
|
|
138
|
+
event.start_ms.is_a?(Integer) &&
|
|
139
|
+
event.start_ms.between?(0, 9_007_199_254_740_991) &&
|
|
140
|
+
event.end_ms.is_a?(Integer) &&
|
|
141
|
+
event.end_ms.between?(event.start_ms, 9_007_199_254_740_991)
|
|
142
|
+
raise(
|
|
143
|
+
ArgumentError,
|
|
144
|
+
"Invalid Live transcript delta: expected an event ID, text and nonnegative timed interval"
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
Fragment.new(speaker, event.delta.dup.freeze, event.start_ms, event.end_ms, now_ms)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def finish(reason)
|
|
152
|
+
@closed = true
|
|
153
|
+
@wake.signal
|
|
154
|
+
emitted = flush_pending + @grouping.close(source_now, reason)
|
|
155
|
+
@seen_ids.clear
|
|
156
|
+
emitted
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def commit(fragments)
|
|
160
|
+
first = fragments.first
|
|
161
|
+
return [] unless first
|
|
162
|
+
|
|
163
|
+
emitted = []
|
|
164
|
+
if fragments.any? { |part| @last_start_ms[part.speaker] && part.start_ms < @last_start_ms[part.speaker] }
|
|
165
|
+
emitted.concat(@grouping.close(source_now, :timestamp_reset))
|
|
166
|
+
@last_start_ms.clear
|
|
167
|
+
@anchor_source_ms = nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
fragments.each { |part| @last_start_ms[part.speaker] = part.start_ms }
|
|
171
|
+
@anchor_source_ms = [@anchor_source_ms || 0, *fragments.map(&:end_ms)].max
|
|
172
|
+
@anchor_received_at = fragments.map(&:received_at).max
|
|
173
|
+
emitted.concat(@grouping.process(fragments))
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def flush_pending
|
|
177
|
+
pending = @pending
|
|
178
|
+
@pending = []
|
|
179
|
+
commit(pending)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def now_ms = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
|
183
|
+
|
|
184
|
+
def source_now
|
|
185
|
+
@anchor_source_ms ? @anchor_source_ms + [0, now_ms - @anchor_received_at].max : 0
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def timer_delay
|
|
189
|
+
delay = if (first = @pending.first)
|
|
190
|
+
first.received_at + 50 - now_ms
|
|
191
|
+
elsif (deadline = @grouping.deadline)
|
|
192
|
+
deadline - source_now
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
@update_due_ms = if @grouping.update_pending?
|
|
196
|
+
@update_due_ms || (now_ms + 50)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
[delay, @update_due_ms && (@update_due_ms - now_ms)].compact.min
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def start_timer
|
|
203
|
+
return if @timer_thread&.alive?
|
|
204
|
+
|
|
205
|
+
@timer_thread = Thread.new { timer_loop }
|
|
206
|
+
@timer_thread.name = "openai-live-transcript"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def timer_loop
|
|
210
|
+
loop do
|
|
211
|
+
should_drain = @lock.synchronize do
|
|
212
|
+
loop do
|
|
213
|
+
return if @closed
|
|
214
|
+
|
|
215
|
+
delay = timer_delay
|
|
216
|
+
if delay && delay <= 0
|
|
217
|
+
break enqueue(flush_pending + @grouping.advance(source_now) + @grouping.flush_update)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
@wake.wait(@lock, delay && (delay / 1000.0))
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
drain if should_drain
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Enqueue while holding the state lock so caller/timer transitions retain
|
|
229
|
+
# their order. One drain owns callback delivery, including reentrant work.
|
|
230
|
+
def enqueue(emitted)
|
|
231
|
+
@updates.concat(emitted)
|
|
232
|
+
return false if @dispatching || @updates.empty?
|
|
233
|
+
|
|
234
|
+
@dispatching = true
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def drain
|
|
238
|
+
drained = false
|
|
239
|
+
loop do
|
|
240
|
+
update = @lock.synchronize do
|
|
241
|
+
if @updates.empty?
|
|
242
|
+
@dispatching = false
|
|
243
|
+
drained = true
|
|
244
|
+
return
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
@updates.shift
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
if update.reason
|
|
251
|
+
@on_segment_closed&.call(ClosedSegment.new(update.segment, update.reason))
|
|
252
|
+
else
|
|
253
|
+
@on_segment_updated&.call(update.segment)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
ensure
|
|
258
|
+
@lock.synchronize { @dispatching = false } unless drained
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
require_relative "transcript_grouping"
|