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.
Files changed (147) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/README.md +5 -2
  4. data/docs/architecture/security-model.md +2 -2
  5. data/examples/audio/record_and_play.rb +1 -1
  6. data/lib/openai/helpers/live/transcript_grouper.rb +264 -0
  7. data/lib/openai/helpers/live/transcript_grouping.rb +309 -0
  8. data/lib/openai/helpers/live/websocket.rb +454 -0
  9. data/lib/openai/helpers/realtime/translation.rb +440 -0
  10. data/lib/openai/helpers/realtime.rb +1 -0
  11. data/lib/openai/helpers/responses_websocket/incremental.rb +246 -0
  12. data/lib/openai/helpers/responses_websocket/transports/async_websocket.rb +183 -0
  13. data/lib/openai/helpers/responses_websocket.rb +1 -0
  14. data/lib/openai/helpers/streaming/response_stream.rb +19 -11
  15. data/lib/openai/helpers/structured_output/chat_completion_parser.rb +0 -2
  16. data/lib/openai/helpers/structured_output/response_parser.rb +1 -2
  17. data/lib/openai/helpers/structured_output/union_of.rb +0 -2
  18. data/lib/openai/helpers/websocket/async_websocket_transport.rb +11 -6
  19. data/lib/openai/helpers/websocket/connection_manager.rb +9 -0
  20. data/lib/openai/internal/read_io_adapter.rb +0 -2
  21. data/lib/openai/internal/stream.rb +0 -2
  22. data/lib/openai/internal/type/base_model.rb +4 -6
  23. data/lib/openai/internal/type/converter.rb +5 -3
  24. data/lib/openai/internal/type/enum.rb +0 -2
  25. data/lib/openai/internal/type/union.rb +0 -6
  26. data/lib/openai/internal/type/unknown.rb +34 -6
  27. data/lib/openai/internal/util.rb +0 -4
  28. data/lib/openai/models/audio/speech_create_params.rb +9 -0
  29. data/lib/openai/models/audio/transcription_create_response.rb +4 -1
  30. data/lib/openai/models/audio/translation_create_response.rb +4 -1
  31. data/lib/openai/models/beta/agent.rb +1 -0
  32. data/lib/openai/models/beta/agent_create_params.rb +3 -0
  33. data/lib/openai/models/beta/agent_session.rb +1 -0
  34. data/lib/openai/models/beta/agent_update_params.rb +3 -0
  35. data/lib/openai/models/beta/agents/environments/template_create_params.rb +3 -3
  36. data/lib/openai/models/beta/agents/environments/template_update_params.rb +6 -6
  37. data/lib/openai/models/beta/agents/session_create_params.rb +3 -0
  38. data/lib/openai/models/beta/agents/session_update_params.rb +3 -0
  39. data/lib/openai/models/beta/agents/vaults/credential.rb +10 -1
  40. data/lib/openai/models/beta/agents/vaults/credential_create_params.rb +12 -1
  41. data/lib/openai/models/beta/agents/vaults/credential_update_params.rb +14 -3
  42. data/lib/openai/models/beta/beta_response.rb +38 -1
  43. data/lib/openai/models/beta/beta_responses_client_event.rb +73 -1
  44. data/lib/openai/models/beta/beta_web_search_preview_tool.rb +9 -3
  45. data/lib/openai/models/beta/beta_web_search_tool.rb +9 -3
  46. data/lib/openai/models/beta/environment_param.rb +3 -3
  47. data/lib/openai/models/beta/response_compact_params.rb +3 -0
  48. data/lib/openai/models/beta/response_create_params.rb +68 -1
  49. data/lib/openai/models/beta/session_turn_error.rb +3 -0
  50. data/lib/openai/models/chat_model.rb +1 -0
  51. data/lib/openai/models/responses/response.rb +35 -1
  52. data/lib/openai/models/responses/response_compact_params.rb +3 -0
  53. data/lib/openai/models/responses/response_create_params.rb +65 -1
  54. data/lib/openai/models/responses/responses_client_event.rb +70 -1
  55. data/lib/openai/models/responses/web_search_preview_tool.rb +9 -3
  56. data/lib/openai/models/responses/web_search_tool.rb +9 -3
  57. data/lib/openai/net_http_client.rb +0 -2
  58. data/lib/openai/resources/audio/transcriptions.rb +5 -3
  59. data/lib/openai/resources/audio/translations.rb +1 -1
  60. data/lib/openai/resources/beta/agents/environments/templates.rb +3 -3
  61. data/lib/openai/resources/beta/agents/vaults/credentials.rb +12 -4
  62. data/lib/openai/resources/beta/responses.rb +8 -2
  63. data/lib/openai/resources/responses.rb +8 -1
  64. data/lib/openai/version.rb +1 -1
  65. data/lib/openai.rb +1 -0
  66. data/rbi/openai/helpers/live/transcript_grouper.rbi +67 -0
  67. data/rbi/openai/helpers/live/websocket.rbi +299 -0
  68. data/rbi/openai/helpers/realtime/translation.rbi +137 -0
  69. data/rbi/openai/helpers/responses_websocket/incremental.rbi +32 -0
  70. data/rbi/openai/helpers/responses_websocket/transports/async_websocket.rbi +24 -0
  71. data/rbi/openai/helpers/streaming/response_stream.rbi +5 -0
  72. data/rbi/openai/internal/type/converter.rbi +3 -2
  73. data/rbi/openai/models/audio/speech_create_params.rbi +3 -0
  74. data/rbi/openai/models/audio/transcription_create_response.rbi +2 -1
  75. data/rbi/openai/models/audio/translation_create_response.rbi +2 -1
  76. data/rbi/openai/models/beta/agent.rbi +1 -0
  77. data/rbi/openai/models/beta/agent_create_params.rbi +3 -0
  78. data/rbi/openai/models/beta/agent_session.rbi +1 -0
  79. data/rbi/openai/models/beta/agent_update_params.rbi +3 -0
  80. data/rbi/openai/models/beta/agents/environments/template_create_params.rbi +3 -3
  81. data/rbi/openai/models/beta/agents/environments/template_update_params.rbi +6 -6
  82. data/rbi/openai/models/beta/agents/session_create_params.rbi +3 -0
  83. data/rbi/openai/models/beta/agents/session_update_params.rbi +3 -0
  84. data/rbi/openai/models/beta/agents/vaults/credential.rbi +10 -0
  85. data/rbi/openai/models/beta/agents/vaults/credential_create_params.rbi +15 -0
  86. data/rbi/openai/models/beta/agents/vaults/credential_update_params.rbi +35 -6
  87. data/rbi/openai/models/beta/beta_response.rbi +64 -0
  88. data/rbi/openai/models/beta/beta_responses_client_event.rbi +128 -0
  89. data/rbi/openai/models/beta/beta_web_search_preview_tool.rbi +9 -3
  90. data/rbi/openai/models/beta/beta_web_search_tool.rbi +9 -3
  91. data/rbi/openai/models/beta/environment_param.rbi +3 -3
  92. data/rbi/openai/models/beta/response_compact_params.rbi +1 -0
  93. data/rbi/openai/models/beta/response_create_params.rbi +100 -0
  94. data/rbi/openai/models/beta/session_turn_error.rbi +6 -0
  95. data/rbi/openai/models/chat_model.rbi +1 -0
  96. data/rbi/openai/models/responses/response.rbi +63 -0
  97. data/rbi/openai/models/responses/response_compact_params.rbi +1 -0
  98. data/rbi/openai/models/responses/response_create_params.rbi +106 -0
  99. data/rbi/openai/models/responses/responses_client_event.rbi +125 -0
  100. data/rbi/openai/models/responses/web_search_preview_tool.rbi +9 -3
  101. data/rbi/openai/models/responses/web_search_tool.rbi +9 -3
  102. data/rbi/openai/resources/audio/transcriptions.rbi +4 -2
  103. data/rbi/openai/resources/beta/agents/environments/templates.rbi +3 -3
  104. data/rbi/openai/resources/beta/agents/vaults/credentials.rbi +11 -3
  105. data/rbi/openai/resources/beta/responses.rbi +6 -0
  106. data/rbi/openai/resources/responses.rbi +11 -0
  107. data/responses-websocket.md +55 -0
  108. data/sig/openai/helpers/live/transcript_grouper.rbs +42 -0
  109. data/sig/openai/helpers/live/websocket.rbs +152 -0
  110. data/sig/openai/helpers/realtime/translation.rbs +81 -0
  111. data/sig/openai/helpers/responses_websocket/incremental.rbs +14 -0
  112. data/sig/openai/helpers/responses_websocket/transports/async_websocket.rbs +21 -0
  113. data/sig/openai/helpers/streaming/response_stream.rbs +9 -0
  114. data/sig/openai/internal/type/converter.rbs +3 -1
  115. data/sig/openai/models/audio/speech_create_params.rbs +6 -0
  116. data/sig/openai/models/audio/transcription_create_response.rbs +1 -0
  117. data/sig/openai/models/audio/translation_create_response.rbs +3 -1
  118. data/sig/openai/models/beta/agent.rbs +3 -1
  119. data/sig/openai/models/beta/agent_create_params.rbs +5 -1
  120. data/sig/openai/models/beta/agent_session.rbs +3 -1
  121. data/sig/openai/models/beta/agent_update_params.rbs +5 -1
  122. data/sig/openai/models/beta/agents/environments/template_create_params.rbs +2 -2
  123. data/sig/openai/models/beta/agents/environments/template_update_params.rbs +4 -4
  124. data/sig/openai/models/beta/agents/session_create_params.rbs +5 -1
  125. data/sig/openai/models/beta/agents/session_update_params.rbs +5 -1
  126. data/sig/openai/models/beta/agents/vaults/credential.rbs +8 -0
  127. data/sig/openai/models/beta/agents/vaults/credential_create_params.rbs +14 -0
  128. data/sig/openai/models/beta/agents/vaults/credential_update_params.rbs +23 -4
  129. data/sig/openai/models/beta/beta_response.rbs +37 -0
  130. data/sig/openai/models/beta/beta_responses_client_event.rbs +79 -0
  131. data/sig/openai/models/beta/beta_web_search_preview_tool.rbs +6 -2
  132. data/sig/openai/models/beta/beta_web_search_tool.rbs +6 -2
  133. data/sig/openai/models/beta/environment_param.rbs +2 -2
  134. data/sig/openai/models/beta/response_compact_params.rbs +2 -0
  135. data/sig/openai/models/beta/response_create_params.rbs +79 -0
  136. data/sig/openai/models/beta/session_turn_error.rbs +4 -0
  137. data/sig/openai/models/chat_model.rbs +2 -0
  138. data/sig/openai/models/responses/response.rbs +35 -0
  139. data/sig/openai/models/responses/response_compact_params.rbs +2 -0
  140. data/sig/openai/models/responses/response_create_params.rbs +77 -0
  141. data/sig/openai/models/responses/responses_client_event.rbs +77 -0
  142. data/sig/openai/models/responses/web_search_preview_tool.rbs +6 -2
  143. data/sig/openai/models/responses/web_search_tool.rbs +6 -2
  144. data/sig/openai/resources/beta/agents/vaults/credentials.rbs +3 -1
  145. data/sig/openai/resources/beta/responses.rbs +2 -0
  146. data/sig/openai/resources/responses.rbs +4 -0
  147. metadata +14 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1a4b5e8a0b4b3e964fa90dca65b45eabfe72a29b443df8dc76a4d0ee3858475
4
- data.tar.gz: 21eee4b1f9cf93ed7516d7466b575f22a4d5191c5b5b03bf50e8dd6f61def78d
3
+ metadata.gz: 45b725bdf2e7a6b6837d634f0a91df36e77e4051d3a44019742cd9176ecf53ec
4
+ data.tar.gz: d42beb5a3f77a5cb8ef597fa8ce4bb80c6a5485252becedbd67ec4e65ef5f674
5
5
  SHA512:
6
- metadata.gz: a2e5e8a2626d87a134c2ad59f52ea2efe95d7fa2eaafab90e06c67058e69a0f9fb7e9f5f291184200c5fbc8a0902db779f721936561171140511b0f9020986f6
7
- data.tar.gz: a0577ce317a33dd4612720e60b26ec3d6cdc02f936270adad3b0c1e3509027338c441fdad4d733ac1aa4318266472cd9281212b0c4266c8fcfc3234b9e9cb551
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.95.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
- See [the contributing documentation](https://github.com/openai/openai-ruby/tree/main/CONTRIBUTING.md).
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:42-50`).
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:42-50` |
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"