openai 0.114.0 → 0.115.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 (226) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/README.md +17 -2
  4. data/docs/architecture/security-model.md +11 -16
  5. data/examples/responses/websocket_workflows.rb +78 -3
  6. data/lib/openai/helpers/chat/tool_runner.rb +1 -1
  7. data/lib/openai/helpers/live/websocket.rb +31 -0
  8. data/lib/openai/helpers/realtime/connection.rb +33 -0
  9. data/lib/openai/helpers/realtime/connection_manager.rb +1 -0
  10. data/lib/openai/helpers/realtime/conversation_item_variant_resolver.rb +5 -0
  11. data/lib/openai/helpers/realtime/recovery.rb +5 -1
  12. data/lib/openai/helpers/realtime/translation.rb +5 -0
  13. data/lib/openai/helpers/realtime/transports/async_websocket.rb +19 -0
  14. data/lib/openai/helpers/responses_websocket/connection.rb +53 -15
  15. data/lib/openai/helpers/responses_websocket/connection_manager.rb +1 -0
  16. data/lib/openai/helpers/responses_websocket/connection_resources.rb +1 -1
  17. data/lib/openai/helpers/structured_output/response_parser.rb +1 -0
  18. data/lib/openai/helpers/structured_output.rb +58 -0
  19. data/lib/openai/helpers/websocket/connection_manager.rb +11 -0
  20. data/lib/openai/helpers/websocket/resource.rb +1 -0
  21. data/lib/openai/helpers/websocket/typed_connection.rb +1 -0
  22. data/lib/openai/internal/type/array_of.rb +1 -1
  23. data/lib/openai/internal/type/hash_of.rb +1 -1
  24. data/lib/openai/internal/type/request_parameters.rb +2 -1
  25. data/lib/openai/internal/type/union.rb +18 -6
  26. data/lib/openai/models/admin/organization/external_key.rb +169 -0
  27. data/lib/openai/models/admin/organization/external_key_create_params.rb +311 -0
  28. data/lib/openai/models/admin/organization/external_key_delete_params.rb +24 -0
  29. data/lib/openai/models/admin/organization/external_key_list_params.rb +69 -0
  30. data/lib/openai/models/admin/organization/external_key_retrieve_params.rb +24 -0
  31. data/lib/openai/models/admin/organization/external_key_validate_params.rb +24 -0
  32. data/lib/openai/models/admin/organization/external_key_validate_response.rb +20 -0
  33. data/lib/openai/models/audio/speech_model.rb +12 -0
  34. data/lib/openai/models/audio_model.rb +15 -0
  35. data/lib/openai/models/beta/agent.rb +49 -1
  36. data/lib/openai/models/beta/agent_create_params.rb +51 -1
  37. data/lib/openai/models/beta/agent_session.rb +75 -2
  38. data/lib/openai/models/beta/agent_update_params.rb +62 -1
  39. data/lib/openai/models/beta/agents/session_create_params.rb +94 -2
  40. data/lib/openai/models/beta/agents/session_list_params.rb +16 -1
  41. data/lib/openai/models/beta/agents/session_update_params.rb +50 -5
  42. data/lib/openai/models/beta/assistant_update_params.rb +18 -0
  43. data/lib/openai/models/beta/beta_response.rb +224 -1
  44. data/lib/openai/models/beta/beta_response_configuration_update_item.rb +64 -3
  45. data/lib/openai/models/beta/beta_response_configuration_update_item_param.rb +64 -4
  46. data/lib/openai/models/beta/beta_response_input_item.rb +2 -3
  47. data/lib/openai/models/beta/beta_response_item.rb +2 -2
  48. data/lib/openai/models/beta/beta_response_tool_search_output_item_param.rb +6 -6
  49. data/lib/openai/models/beta/beta_responses_client_event.rb +20 -1
  50. data/lib/openai/models/beta/beta_tool.rb +6 -6
  51. data/lib/openai/models/beta/response_compact_params.rb +6 -0
  52. data/lib/openai/models/beta/response_create_params.rb +6 -0
  53. data/lib/openai/models/chat_model.rb +6 -0
  54. data/lib/openai/models/conversations/conversation_item.rb +2 -2
  55. data/lib/openai/models/image_edit_params.rb +46 -25
  56. data/lib/openai/models/live/fork_server_event.rb +43 -3
  57. data/lib/openai/models/live/server_event.rb +43 -3
  58. data/lib/openai/models/live/sideband_client_event.rb +40 -1
  59. data/lib/openai/models/live/sideband_server_event.rb +43 -3
  60. data/lib/openai/models/realtime/audio_transcription.rb +15 -0
  61. data/lib/openai/models/realtime/conversation_item_added.rb +2 -2
  62. data/lib/openai/models/realtime/conversation_item_create_event.rb +2 -2
  63. data/lib/openai/models/realtime/conversation_item_created_event.rb +2 -2
  64. data/lib/openai/models/realtime/conversation_item_done.rb +2 -2
  65. data/lib/openai/models/realtime/realtime_response.rb +2 -2
  66. data/lib/openai/models/realtime/realtime_response_create_params.rb +2 -2
  67. data/lib/openai/models/realtime/realtime_server_event.rb +2 -2
  68. data/lib/openai/models/realtime/realtime_session.rb +18 -0
  69. data/lib/openai/models/realtime/realtime_session_create_request.rb +18 -0
  70. data/lib/openai/models/realtime/realtime_session_create_response.rb +18 -0
  71. data/lib/openai/models/realtime/response_output_item_added_event.rb +2 -2
  72. data/lib/openai/models/realtime/response_output_item_done_event.rb +2 -2
  73. data/lib/openai/models/responses/response.rb +221 -1
  74. data/lib/openai/models/responses/response_compact_params.rb +6 -0
  75. data/lib/openai/models/responses/response_configuration_update_item.rb +50 -3
  76. data/lib/openai/models/responses/response_configuration_update_item_param.rb +50 -4
  77. data/lib/openai/models/responses/response_input_item.rb +2 -3
  78. data/lib/openai/models/responses/response_item.rb +2 -2
  79. data/lib/openai/models/responses/response_tool_search_output_item_param.rb +6 -6
  80. data/lib/openai/models/responses/responses_client_event.rb +14 -1
  81. data/lib/openai/models/responses/tool.rb +6 -6
  82. data/lib/openai/net_http_client.rb +6 -0
  83. data/lib/openai/providers/vertex.rb +0 -10
  84. data/lib/openai/resources/admin/organization/external_keys.rb +132 -0
  85. data/lib/openai/resources/admin/organization.rb +4 -0
  86. data/lib/openai/resources/audio/speech.rb +8 -0
  87. data/lib/openai/resources/audio/translations.rb +8 -0
  88. data/lib/openai/resources/beta/agents/sessions.rb +22 -6
  89. data/lib/openai/resources/beta/agents.rb +17 -2
  90. data/lib/openai/resources/images.rb +36 -18
  91. data/lib/openai/version.rb +1 -1
  92. data/lib/openai.rb +8 -0
  93. data/rbi/openai/helpers/live/websocket.rbi +1 -12
  94. data/rbi/openai/helpers/realtime/connection.rbi +5 -0
  95. data/rbi/openai/helpers/realtime/transports/async_websocket.rbi +5 -0
  96. data/rbi/openai/helpers/responses_websocket/connection.rbi +7 -2
  97. data/rbi/openai/helpers/structured_output.rbi +10 -0
  98. data/rbi/openai/internal/type/array_of.rbi +1 -1
  99. data/rbi/openai/internal/type/hash_of.rbi +1 -1
  100. data/rbi/openai/internal/type/union.rbi +5 -0
  101. data/rbi/openai/models/admin/organization/external_key.rbi +301 -0
  102. data/rbi/openai/models/admin/organization/external_key_create_params.rbi +532 -0
  103. data/rbi/openai/models/admin/organization/external_key_delete_params.rbi +57 -0
  104. data/rbi/openai/models/admin/organization/external_key_list_params.rbi +129 -0
  105. data/rbi/openai/models/admin/organization/external_key_retrieve_params.rbi +57 -0
  106. data/rbi/openai/models/admin/organization/external_key_validate_params.rbi +57 -0
  107. data/rbi/openai/models/admin/organization/external_key_validate_response.rbi +50 -0
  108. data/rbi/openai/models/beta/agent.rbi +103 -1
  109. data/rbi/openai/models/beta/agent_create_params.rbi +109 -0
  110. data/rbi/openai/models/beta/agent_session.rbi +136 -3
  111. data/rbi/openai/models/beta/agent_update_params.rbi +120 -0
  112. data/rbi/openai/models/beta/agents/session_create_params.rbi +153 -0
  113. data/rbi/openai/models/beta/agents/session_list_params.rbi +19 -0
  114. data/rbi/openai/models/beta/agents/session_update_params.rbi +94 -6
  115. data/rbi/openai/models/beta/beta_response.rbi +394 -0
  116. data/rbi/openai/models/beta/beta_response_configuration_update_item.rbi +113 -3
  117. data/rbi/openai/models/beta/beta_response_configuration_update_item_param.rbi +125 -4
  118. data/rbi/openai/models/beta/beta_response_tool_search_output_item_param.rbi +6 -6
  119. data/rbi/openai/models/beta/beta_responses_client_event.rbi +17 -0
  120. data/rbi/openai/models/beta/beta_tool.rbi +6 -6
  121. data/rbi/openai/models/decision_create_params.rbi +4 -1
  122. data/rbi/openai/models/image_edit_params.rbi +49 -24
  123. data/rbi/openai/models/live/fork_server_event.rbi +65 -1
  124. data/rbi/openai/models/live/server_event.rbi +65 -1
  125. data/rbi/openai/models/live/sideband_client_event.rbi +60 -0
  126. data/rbi/openai/models/live/sideband_server_event.rbi +65 -1
  127. data/rbi/openai/models/realtime/conversation_item_added.rbi +6 -3
  128. data/rbi/openai/models/realtime/conversation_item_create_event.rbi +6 -3
  129. data/rbi/openai/models/realtime/conversation_item_created_event.rbi +6 -3
  130. data/rbi/openai/models/realtime/conversation_item_done.rbi +6 -3
  131. data/rbi/openai/models/realtime/realtime_response.rbi +8 -4
  132. data/rbi/openai/models/realtime/realtime_response_create_params.rbi +8 -4
  133. data/rbi/openai/models/realtime/realtime_server_event.rbi +6 -3
  134. data/rbi/openai/models/realtime/response_output_item_added_event.rbi +6 -3
  135. data/rbi/openai/models/realtime/response_output_item_done_event.rbi +6 -3
  136. data/rbi/openai/models/responses/response.rbi +394 -0
  137. data/rbi/openai/models/responses/response_configuration_update_item.rbi +76 -3
  138. data/rbi/openai/models/responses/response_configuration_update_item_param.rbi +74 -4
  139. data/rbi/openai/models/responses/response_tool_search_output_item_param.rbi +6 -6
  140. data/rbi/openai/models/responses/responses_client_event.rbi +17 -0
  141. data/rbi/openai/models/responses/tool.rbi +6 -6
  142. data/rbi/openai/net_http_client.rbi +5 -0
  143. data/rbi/openai/resources/admin/organization/external_keys.rbi +86 -0
  144. data/rbi/openai/resources/admin/organization.rbi +3 -0
  145. data/rbi/openai/resources/audio/speech.rbi +8 -0
  146. data/rbi/openai/resources/audio/translations.rbi +6 -0
  147. data/rbi/openai/resources/beta/agents/sessions.rbi +18 -2
  148. data/rbi/openai/resources/beta/agents.rbi +15 -0
  149. data/rbi/openai/resources/images.rbi +34 -16
  150. data/rbi/openai/resources/responses.rbi +2 -0
  151. data/realtime.md +6 -0
  152. data/responses-websocket.md +53 -0
  153. data/sig/openai/helpers/live/websocket.rbs +1 -9
  154. data/sig/openai/helpers/realtime/connection.rbs +3 -0
  155. data/sig/openai/helpers/realtime/transports/async_websocket.rbs +3 -0
  156. data/sig/openai/helpers/responses_websocket/connection.rbs +7 -1
  157. data/sig/openai/helpers/structured_output.rbs +9 -0
  158. data/sig/openai/internal/type/union.rbs +4 -0
  159. data/sig/openai/models/admin/organization/external_key.rbs +186 -0
  160. data/sig/openai/models/admin/organization/external_key_create_params.rbs +423 -0
  161. data/sig/openai/models/admin/organization/external_key_delete_params.rbs +27 -0
  162. data/sig/openai/models/admin/organization/external_key_list_params.rbs +81 -0
  163. data/sig/openai/models/admin/organization/external_key_retrieve_params.rbs +27 -0
  164. data/sig/openai/models/admin/organization/external_key_validate_params.rbs +27 -0
  165. data/sig/openai/models/admin/organization/external_key_validate_response.rbs +17 -0
  166. data/sig/openai/models/admin/organization/external_storage_create_params.rbs +72 -2
  167. data/sig/openai/models/audio/speech_create_params.rbs +15 -2
  168. data/sig/openai/models/audio/transcription_create_params.rbs +30 -2
  169. data/sig/openai/models/audio/voice_create_params.rbs +47 -2
  170. data/sig/openai/models/beta/agent.rbs +54 -2
  171. data/sig/openai/models/beta/agent_create_params.rbs +54 -0
  172. data/sig/openai/models/beta/agent_session.rbs +82 -3
  173. data/sig/openai/models/beta/agent_update_params.rbs +64 -0
  174. data/sig/openai/models/beta/agents/session_create_params.rbs +91 -0
  175. data/sig/openai/models/beta/agents/session_list_params.rbs +20 -0
  176. data/sig/openai/models/beta/agents/session_update_params.rbs +48 -6
  177. data/sig/openai/models/beta/assistant_create_params.rbs +5 -2
  178. data/sig/openai/models/beta/beta_response.rbs +223 -0
  179. data/sig/openai/models/beta/beta_response_configuration_update_item.rbs +58 -2
  180. data/sig/openai/models/beta/beta_response_configuration_update_item_param.rbs +58 -2
  181. data/sig/openai/models/beta/beta_response_tool_search_output_item_param.rbs +4 -4
  182. data/sig/openai/models/beta/beta_responses_client_event.rbs +17 -0
  183. data/sig/openai/models/beta/beta_tool.rbs +4 -4
  184. data/sig/openai/models/beta/response_create_params.rbs +5 -2
  185. data/sig/openai/models/beta/responses/input_token_count_params.rbs +10 -2
  186. data/sig/openai/models/beta/thread_create_and_run_params.rbs +11 -5
  187. data/sig/openai/models/beta/thread_create_params.rbs +11 -5
  188. data/sig/openai/models/beta/threads/message_create_params.rbs +6 -3
  189. data/sig/openai/models/beta/threads/run_create_params.rbs +6 -3
  190. data/sig/openai/models/decision_create_params.rbs +27 -2
  191. data/sig/openai/models/eval_create_params.rbs +25 -4
  192. data/sig/openai/models/evals/run_create_params.rbs +52 -8
  193. data/sig/openai/models/image_edit_params.rbs +47 -16
  194. data/sig/openai/models/live/fork_server_event.rbs +39 -0
  195. data/sig/openai/models/live/server_event.rbs +39 -0
  196. data/sig/openai/models/live/session_accept_params.rbs +39 -2
  197. data/sig/openai/models/live/sideband_client_event.rbs +32 -0
  198. data/sig/openai/models/live/sideband_server_event.rbs +39 -0
  199. data/sig/openai/models/realtime/conversation_item.rbs +1 -0
  200. data/sig/openai/models/responses/input_token_count_params.rbs +10 -2
  201. data/sig/openai/models/responses/response.rbs +223 -0
  202. data/sig/openai/models/responses/response_configuration_update_item.rbs +45 -2
  203. data/sig/openai/models/responses/response_configuration_update_item_param.rbs +45 -2
  204. data/sig/openai/models/responses/response_create_params.rbs +5 -2
  205. data/sig/openai/models/responses/response_tool_search_output_item_param.rbs +4 -4
  206. data/sig/openai/models/responses/responses_client_event.rbs +17 -0
  207. data/sig/openai/models/responses/tool.rbs +4 -4
  208. data/sig/openai/models/video_edit_params.rbs +11 -2
  209. data/sig/openai/models/video_extend_params.rbs +11 -2
  210. data/sig/openai/net_http_client.rbs +3 -0
  211. data/sig/openai/resources/admin/organization/external_keys.rbs +70 -0
  212. data/sig/openai/resources/admin/organization/external_storage.rbs +23 -1
  213. data/sig/openai/resources/admin/organization.rbs +2 -0
  214. data/sig/openai/resources/audio/speech.rbs +2 -1
  215. data/sig/openai/resources/audio/transcriptions.rbs +14 -2
  216. data/sig/openai/resources/audio/voices.rbs +14 -1
  217. data/sig/openai/resources/beta/agents/sessions.rbs +4 -0
  218. data/sig/openai/resources/beta/agents.rbs +4 -0
  219. data/sig/openai/resources/beta/responses/input_tokens.rbs +2 -1
  220. data/sig/openai/resources/beta/responses.rbs +4 -2
  221. data/sig/openai/resources/decisions.rbs +7 -1
  222. data/sig/openai/resources/images.rbs +2 -0
  223. data/sig/openai/resources/responses/input_tokens.rbs +2 -1
  224. data/sig/openai/resources/responses.rbs +5 -2
  225. data/sig/openai/resources/videos.rbs +2 -2
  226. metadata +25 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f8905b3e720546308f3334931a036c17e3e990e66ec863039da6c661d6024dec
4
- data.tar.gz: 4698d4841fd6becc4266b58c8b863e3fb7b50a16d4b76523808ac291e5c566f3
3
+ metadata.gz: 5ced72b8e35330a5d4832f6e8a19c4b989c320faea258dcae427e0df5daa5af7
4
+ data.tar.gz: 346d52cbceace88063f31b32abb7f3e3f0a6c85da9315444335534f5fc3d554c
5
5
  SHA512:
6
- metadata.gz: e664a8f9ee816f4deb836f3b03b1d82a5178f9d8e85fa42a25530e1b3d514273dbefd17da517837b5ef197e51c0e16abcf85c845491744278339903abc3b1502
7
- data.tar.gz: 6398277b839b172914d849d071a4994d8a2c0e90ef74cde85ce76f27c940753b000e0f709f05a1d1e0f9ab8fdb7c44ce3cb77f5f6f8fcde257fbd4cdc466673c
6
+ metadata.gz: 664188f40a63883c74614050aa814ee2f207135c74856c0e132df3452f9d42a54f01f23133872459ef91fd2d078f11ca01f2bb4e008e59eb53b780e611978435
7
+ data.tar.gz: cc89bc9ed7879748bba0bc27f3f4ee84c963faed1dc5305016d8d57b590a85eafb48e5c3dc65a9aa24f012a3285df4858e621471687292f3058c886164c3892c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.115.0](https://github.com/openai/openai-ruby/compare/v0.114.0...v0.115.0) (2026-10-31)
4
+
5
+
6
+ ### Features
7
+
8
+ * **api:** add agent context compaction settings ([#1003](https://github.com/openai/openai-ruby/issues/1003)) ([6f5b0a3](https://github.com/openai/openai-ruby/commit/6f5b0a3454aed204ccd8baad2ff0fb8bca1c17a5))
9
+ * **api:** add agent session review and tool settings ([#1013](https://github.com/openai/openai-ruby/issues/1013)) ([b604fe7](https://github.com/openai/openai-ruby/commit/b604fe7ad3b252adcc3200314a46a028c9ad9a9b))
10
+ * **api:** add experimental agent time awareness ([#1005](https://github.com/openai/openai-ruby/issues/1005)) ([25ab8e9](https://github.com/openai/openai-ruby/commit/25ab8e922a4f5f1adf74390139d08f8cb9f21db8))
11
+ * **api:** add image edit markup overlays ([#1004](https://github.com/openai/openai-ruby/issues/1004)) ([79e7168](https://github.com/openai/openai-ruby/commit/79e7168b33bccf79d66700b0ac9820cb80d7ef36))
12
+ * **api:** add organization external key management ([#998](https://github.com/openai/openai-ruby/issues/998)) ([e0313fc](https://github.com/openai/openai-ruby/commit/e0313fc261c816fba0628d4fc6e576b674e1d9df))
13
+ * **api:** add SIP DTMF commands and delivery events ([#1002](https://github.com/openai/openai-ruby/issues/1002)) ([0ed1bc6](https://github.com/openai/openai-ruby/commit/0ed1bc69dd8249544631fe7335495f3807a46fd3))
14
+ * **api:** complete request input type definitions ([#999](https://github.com/openai/openai-ruby/issues/999)) ([543fea3](https://github.com/openai/openai-ruby/commit/543fea32e06ee61b2ad5a201fe988120c646b75b))
15
+ * **api:** expose hosted tool usage on responses ([#995](https://github.com/openai/openai-ruby/issues/995)) ([429e9ad](https://github.com/openai/openai-ruby/commit/429e9ad7b436e8c4bc85ea30f5e109f191393f93))
16
+ * **api:** filter agent sessions by metadata ([#992](https://github.com/openai/openai-ruby/issues/992)) ([9dc8a3d](https://github.com/openai/openai-ruby/commit/9dc8a3dd0148cf6e08d18f0b1e0fca8d121311d8))
17
+ * **api:** improve WebSocket encoding and recovery ([#1007](https://github.com/openai/openai-ruby/issues/1007)) ([d1a4b87](https://github.com/openai/openai-ruby/commit/d1a4b87292b52a5a718d7ed40ce6d68750c16ada))
18
+ * **api:** support Responses WebSocket warmup ([#1012](https://github.com/openai/openai-ruby/issues/1012)) ([db09dba](https://github.com/openai/openai-ruby/commit/db09dba45a16e329c436df599e453be74d97b1ad))
19
+ * **api:** support text formats on Responses configuration updates ([#1006](https://github.com/openai/openai-ruby/issues/1006)) ([c8b295c](https://github.com/openai/openai-ruby/commit/c8b295cc209fabc4e52db9bf321176c525f67cc9))
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * accept omitted nullable text in chat tool responses ([#993](https://github.com/openai/openai-ruby/issues/993)) ([b276766](https://github.com/openai/openai-ruby/commit/b2767663f881b47aae65fb1d9d452d64a2ec0cab))
25
+ * preserve Vertex API root for stored responses ([#984](https://github.com/openai/openai-ruby/issues/984)) ([5bcd40b](https://github.com/openai/openai-ruby/commit/5bcd40b01cecb83a1e0a2bc044f7c7902ec0de7f))
26
+ * **realtime:** allow developer messages in response input ([#978](https://github.com/openai/openai-ruby/issues/978)) ([ced5c63](https://github.com/openai/openai-ruby/commit/ced5c6315a9ad411a0d9704df9405c5a600ba55b))
27
+ * **realtime:** reject overlapping reads on physical WebSockets ([#1011](https://github.com/openai/openai-ruby/issues/1011)) ([5317eb4](https://github.com/openai/openai-ruby/commit/5317eb42a544ddbc6ca3950d828f601edfbd0f4c))
28
+ * reject overlapping Live WebSocket reads ([#1009](https://github.com/openai/openai-ruby/issues/1009)) ([e669431](https://github.com/openai/openai-ruby/commit/e669431961a0f14bc2c20ec83d167d701b2df3a0))
29
+ * require explicit WebSocket transport for custom HTTP clients ([#1008](https://github.com/openai/openai-ruby/issues/1008)) ([54bb670](https://github.com/openai/openai-ruby/commit/54bb67010e46d25921f8ff33731640954f047ece))
30
+
31
+
32
+ ### Chores
33
+
34
+ * **api:** document upcoming model shutdowns ([#990](https://github.com/openai/openai-ruby/issues/990)) ([ace1351](https://github.com/openai/openai-ruby/commit/ace13514e2db8f3a0f3d6b5a409d76e08e3b1bc1))
35
+ * **api:** preserve organization external key request contracts ([#1001](https://github.com/openai/openai-ruby/issues/1001)) ([7ea9e1d](https://github.com/openai/openai-ruby/commit/7ea9e1dd9926ae5cbf692aecbedbc4f6c5f6c59a))
36
+ * **api:** update image-model documentation ([#985](https://github.com/openai/openai-ruby/issues/985)) ([02c1d66](https://github.com/openai/openai-ruby/commit/02c1d66449a2ddda2ad64d0edf425795c1ee3f5c))
37
+ * enforce generated-file ownership ([#979](https://github.com/openai/openai-ruby/issues/979)) ([300d5d2](https://github.com/openai/openai-ruby/commit/300d5d2889714a436dd73421f510b7b3b97f8379))
38
+
3
39
  ## [0.114.0](https://github.com/openai/openai-ruby/compare/v0.113.0...v0.114.0) (2026-10-27)
4
40
 
5
41
 
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.114.0"
18
+ gem "openai", "~> 0.115.0"
19
19
  ```
20
20
 
21
21
  <!-- x-release-please-end -->
@@ -226,7 +226,12 @@ client.responses.connect do |connection|
226
226
 
227
227
  connection.each do |event|
228
228
  print(event.delta) if event.type == :"response.output_text.delta"
229
- break if event.type == :"response.completed"
229
+ case event.type
230
+ when :"response.completed"
231
+ break
232
+ when :"response.failed", :"response.incomplete", :error
233
+ raise "Responses operation did not complete."
234
+ end
230
235
  end
231
236
  end
232
237
  ```
@@ -453,6 +458,16 @@ origin, as above, so they cannot be presented elsewhere. Use the
453
458
  Server trust remains separate from the client identity and can be customized
454
459
  with `Net::HTTP` properties such as `cert_store` or `ca_file`.
455
460
 
461
+ An HTTP client's connection settings do not carry over to the default
462
+ WebSocket transport. If you configure `http_client:` with a custom requester
463
+ or customized `OpenAI::NetHTTPClient`, pass a compatible `transport:` when
464
+ opening Responses, Realtime, Translation, or Live WebSockets. The SDK checks
465
+ this before fetching credentials or opening a connection. The supplied
466
+ WebSocket transport must enforce the connection settings your application
467
+ requires; the SDK still validates its supported authentication and provider
468
+ scope. See the [Realtime custom transport contract](realtime.md#custom-transport)
469
+ and [Responses transport options](responses-websocket.md).
470
+
456
471
  Each `OpenAI::NetHTTPClient` owns its connection pool, so separate HTTP client
457
472
  instances do not share connections or credentials.
458
473
 
@@ -1,5 +1,10 @@
1
1
  # OpenAI Ruby SDK Security Model
2
2
 
3
+ Generated files are updated through authoritative inputs or the compiler and
4
+ regeneration; handwritten edits to them are forbidden. Code review enforces this
5
+ ownership rule. The customization-budget reporter and publishers are retired
6
+ and are not part of the active CI or security controls.
7
+
3
8
  This is the canonical detailed threat model for this repository. `SECURITY.md`
4
9
  remains the authority for disclosure and reportability; this document is the
5
10
  authority for architecture, trust boundaries, security invariants, attack
@@ -7,6 +12,11 @@ surface, and severity calibration. Codex Security scans must load this file
7
12
  from the scanned revision instead of relying on a duplicated cloud-configured
8
13
  model.
9
14
 
15
+ Generated-file ownership currently relies on `AGENTS.md`, contributor guidance,
16
+ and ordinary SDK code review. There is no dedicated generated-file edit check or
17
+ required gate. Review generated output and inventory changes together; fix
18
+ violations in the inputs/compiler and regenerate.
19
+
10
20
  ## 1. Overview
11
21
 
12
22
  The OpenAI Ruby gem is a caller-embedded Ruby 3.3+ library, not a hosted
@@ -23,7 +33,6 @@ requests, then parses remote responses into typed Ruby values
23
33
  | Streaming and Realtime helpers | Construct authenticated WebSocket handshakes, enforce transport policy, and parse SSE/WebSocket JSON into typed events | `lib/openai/internal/stream.rb:19-60`, `lib/openai/helpers/realtime/client_extension.rb:167-280`, `lib/openai/helpers/realtime/transports/async_websocket.rb:185-308`, `lib/openai/helpers/realtime/connection.rb:47-100` |
24
34
  | Webhook helper | Verify timestamp and HMAC before parsing webhook events | `lib/openai/resources/webhooks.rb:15-119` |
25
35
  | Providers and workload identity | Azure, Bedrock, built-in subject-token acquisition, exchange, and X.509 credential paths | `lib/openai/providers/azure.rb:40-58`, `lib/openai/auth/subject_token_providers/gcp_id_token_provider.rb:9-67`, `lib/openai/auth/subject_token_providers/azure_managed_identity_token_provider.rb:9-92`, `lib/openai/auth/subject_token_providers/k8s_service_account_token_provider.rb:9-37`, `lib/openai/auth/workload_identity_auth.rb:280-330` |
26
- | CI and release workflows | Run repository code, upload CodeQL results, publish trusted Castiron results, live checks/examples, and releases | `.github/workflows/ci.yml:18-32`, `.github/workflows/codeql.yml:3-53`, `.github/workflows/castiron-custom-code-comment.yml:4-38`, `.github/workflows/examples-e2e.yml:1-43`, `.github/workflows/create-releases.yml:14-79` |
27
36
 
28
37
  ```mermaid
29
38
  flowchart LR
@@ -36,9 +45,6 @@ flowchart LR
36
45
  PR[Candidate checkout] --> CI[Ordinary read-only CI]
37
46
  PR --> CodeQL[CodeQL analysis]
38
47
  CodeQL --> SecurityEvents[Code-scanning results]
39
- PR --> Castiron[Candidate Castiron workflow]
40
- Castiron --> Trusted[Main-sourced workflow_run publisher]
41
- Trusted --> GitHub[Statuses and PR comments]
42
48
  Main[Main revision] --> Live[Live / release workflows]
43
49
  Live --> Secrets[Referenced credentials and publishing authority]
44
50
  ```
@@ -57,7 +63,6 @@ flowchart LR
57
63
  | Dependency acquisition and installation | Gem source, locked revision, transitive gem, native extension, and install/build-script execution | Contributor/CI `Gemfile` selects `syntax_tree-rbs` from GitHub branch `main` and `Gemfile.lock` pins revision `247832988a850b8df050cf207f652872fda49973`; Bedrock CI selects `aws-sdk-core` through `gemfiles/bedrock.gemfile` and its lock; docs lockfiles and gemspec dependencies select other installed code | Locked RubyGems and Git sources plus package metadata from the reviewed revision | CI runners and consumer Bundler/RubyGems installations | Human/source review of dependency and lockfile changes; CI verifies the reviewed checkout but does not make an unreviewed dependency trustworthy | `AGENTS.md:39-42`, `Gemfile:3-15`, `Gemfile.lock:1-20`, `gemfiles/bedrock.gemfile:1-5`, `gemfiles/bedrock.gemfile.lock:1-58`, `.github/workflows/ci-checks.yml:124-144`, `openai.gemspec:18-48`, `docs/Gemfile:1-8`, `docs/Gemfile.lock:1-21` |
58
64
  | Ordinary PR CI | Candidate checkout execution | Checked-out PR revision runs lint, typecheck, build, and tests | Candidate code executes with repository-code authority | Ephemeral CI runner | `permissions: {}`, `contents: read`, `persist-credentials: false` | `.github/workflows/ci.yml:18-32`, `.github/workflows/ci-checks.yml:100-120` |
59
65
  | CodeQL PR analysis | Candidate checkout and code-scanning result upload | Pull requests, main pushes, and merge-group checks run action and Ruby analysis | Candidate source produces SARIF under runner temp; `security-events: write` uploads code-scanning results | GitHub code scanning | Top-level `permissions: {}`; job grants only `actions: read`, `contents: read`, and `security-events: write`; pinned checkout/CodeQL actions; `persist-credentials: false`; checked SARIF must exist and contain no findings | `.github/workflows/codeql.yml:3-28`, `.github/workflows/codeql.yml:37-53`, `.github/workflows/codeql.yml:55-96` |
60
- | Trusted Castiron workflow-run publisher | Commit-status and PR-comment writes | Candidate-associated `workflow_run` metadata -> main-sourced reporter recomputes from current Git objects for statuses and successful report comments; fallback failure comments use event/path, current-head, and monotonic-replacement checks | `statuses: write` and `pull-requests: write` only in publisher jobs | GitHub commit statuses and PR comments | Status path: no candidate artifact, run identity validation, exact head/base checks; successful comment path: trusted report artifact; fallback failure-comment path: main-sourced code, event/path filtering, current-head matching, monotonic replacement | `.github/workflows/castiron-custom-code-comment.yml:4-38`, `.github/workflows/castiron-custom-code-comment.yml:40-98`, `.github/workflows/castiron-custom-code-comment.yml:116-171`, `.github/workflows/castiron-custom-code-comment.yml:174-237` |
61
66
  | Live/release workflows | API keys, X.509 material, GitHub App private key, generated GitHub App token, RubyGems OIDC | Main-only conditions plus named environments; release job passes `OPENAI_SDKS_APP_PRIVATE_KEY` to pinned token action, then passes generated token to Release Please | Referenced long-lived private key as action input; generated short-lived release token; publishing OIDC | Main revision, token action, OpenAI API, GitHub, RubyGems | Repository/ref conditions, separate named environments, least privilege, pinned actions, generated token handoff, OIDC only for publishing | `.github/workflows/live-smoke.yml:17-92`, `.github/workflows/create-releases.yml:17-79` |
62
67
  | Examples E2E workflow | Live-example API credential and uploaded reports | Manual dispatch; main/repository condition; named `ci` environment; `OPENAI_API_KEY` secret reference | Credential only in the live-example step; allowlisted `report.json` and `summary.md` under runner temp | OpenAI API and GitHub artifact readers | `permissions: {}`, `contents: read`, checkout without persisted credentials, main/repository condition, allowlisted reports, 14-day retention, focused confidentiality/isolation tests | `.github/workflows/examples-e2e.yml:1-43`, `test/scripts/examples_e2e_test.rb:202-234`, `test/scripts/examples_e2e_test.rb:306-327` |
63
68
 
@@ -188,16 +193,7 @@ Important boundaries:
188
193
  (`.github/workflows/examples-e2e.yml:1-43`,
189
194
  `test/scripts/examples_e2e_test.rb:202-234`,
190
195
  `test/scripts/examples_e2e_test.rb:306-327`).
191
- - Candidate-associated `workflow_run` metadata and Git objects crossing into
192
- the trusted Castiron publisher are a genuine write-capable boundary. The
193
- status path must preserve run-identity and exact-head/base checks; successful
194
- report comments must remain bound to the trusted report artifact. The
195
- fallback failure-comment path has narrower main-sourced event/path,
196
- current-head, and monotonic-replacement checks that scans must assess
197
- independently
198
- (`.github/workflows/castiron-custom-code-comment.yml:34-98`,
199
- `.github/workflows/castiron-custom-code-comment.yml:116-171`,
200
- `.github/workflows/castiron-custom-code-comment.yml:174-237`).
196
+
201
197
  - Dependency declarations and locks cross a supply-chain boundary when Bundler
202
198
  or RubyGems fetches and executes selected package code in CI or consumer
203
199
  applications. A changed source, Git revision, transitive gem, native
@@ -261,7 +257,6 @@ The following are hypotheses and review guidance, not confirmed findings.
261
257
  | High | Candidate PR code reaches live/release credentials or publishing authority | Privileged workflow executes candidate code or exposes secrets/write tokens | Credential theft or release compromise | Main-only conditions, named environments, least-privilege permissions, trusted publisher separation | Keep candidate and live/release workflows isolated | `.github/workflows/live-smoke.yml:17-92`, `.github/workflows/create-releases.yml:17-79` |
262
258
  | High | Release GitHub App private key reaches an unintended action, log, or artifact | Main/repository release gate or pinned token-action input boundary regresses | Attacker can mint GitHub App tokens beyond one workflow token lifetime | Main/repository condition, named release environment, empty job permissions, pinned token action, generated token passed separately to Release Please | Keep the private key confined to token minting and preserve generated-token handoff | `.github/workflows/create-releases.yml:17-46` |
263
259
  | Medium | Candidate source or generated SARIF influences the write-capable CodeQL upload outside its intended code-scanning result path | PR-adjacent CodeQL action or token scoping regresses | Forged or unintended code-scanning results | Top-level empty permissions; scoped `security-events: write`; read-only content/metadata; pinned actions; no persisted checkout credentials; SARIF existence/shape/finding checks | Preserve least privilege and keep upload input limited to CodeQL output | `.github/workflows/codeql.yml:3-28`, `.github/workflows/codeql.yml:37-53`, `.github/workflows/codeql.yml:55-96` |
264
- | High | Candidate-associated workflow metadata or Git objects influence the write-capable Castiron publisher without the path-specific trusted validation | Attacker controls a PR head or associated workflow metadata and a status, successful-comment, or fallback-comment check regresses | Forged or stale commit statuses and PR comments | Statuses use main-sourced recomputation plus run/head/base checks; successful comments use a trusted report artifact; fallback comments rely on main-sourced event/path, current-head, and monotonic-replacement checks | Preserve each path's distinct recomputation, freshness, artifact-binding, or fallback-isolation checks before its write | `.github/workflows/castiron-custom-code-comment.yml:34-98`, `.github/workflows/castiron-custom-code-comment.yml:116-171`, `.github/workflows/castiron-custom-code-comment.yml:174-237` |
265
260
  | High | A dependency declaration or lockfile selects malicious package code, native extension, or install/build script | Lower-trust change alters a source, Git revision, transitive dependency, or executable package hook that CI or a consumer installs; the contributor/CI bundle currently includes Git-sourced `syntax_tree-rbs` locked at `247832988a850b8df050cf207f652872fda49973`; Bedrock CI separately installs its `aws-sdk-core` bundle | Code execution in CI or consumer applications | Reviewed lockfiles and dependency policy; ordinary and Bedrock CI execute selected dependencies with runner authority | Review direct/transitive dependencies, sources, locked revisions, native extensions, and scripts before acceptance | `AGENTS.md:39-42`, `Gemfile:3-15`, `Gemfile.lock:1-20`, `gemfiles/bedrock.gemfile:1-5`, `gemfiles/bedrock.gemfile.lock:1-58`, `.github/workflows/ci-checks.yml:124-144`, `openai.gemspec:18-48`, `docs/Gemfile:1-8`, `docs/Gemfile.lock:1-21` |
266
261
  | High | Built-in subject-token acquisition or exchange sends a token to an unintended source, proxy, issuer, or origin | Lower-trust configuration, caller-owned environment proxy, local token path, metadata response, or endpoint handling is bypassed | Credential disclosure or forged authentication | Fixed GCP metadata host/path/header with configurable audience/timeout; fixed Azure IMDS endpoint/header with configurable resource/selectors/API version/timeout and JSON token extraction; inherited Net::HTTP environment-proxy routing remains caller-owned; configured/default Kubernetes path with mapped failures; fixed/default issuer paths, origin checks, X.509 destination restrictions | Preserve strict source, proxy, destination, timeout, redirect, and response-parsing handling | `lib/openai/auth/subject_token_providers/gcp_id_token_provider.rb:9-67`, `lib/openai/auth/subject_token_providers/azure_managed_identity_token_provider.rb:9-92`, `lib/openai/auth/subject_token_providers/k8s_service_account_token_provider.rb:9-37`, `lib/openai/auth/workload_identity_auth.rb:303-330`, `lib/openai/providers/bedrock.rb:398-427` |
267
262
  | High | Realtime bearer credentials, proxy credentials, or trace data reach an unintended destination or audience | Lower-trust destination, proxy, TLS, or tracing behavior bypasses handshake controls | Credential disclosure or authenticated connection to the wrong peer | Base-URL/provider restrictions, proxy-header stripping, WSS verification, proxy handling, trace redaction | Preserve destination/TLS/proxy/redaction controls independently from event parsing | `lib/openai/helpers/realtime/client_extension.rb:167-280`, `lib/openai/helpers/realtime/transports/async_websocket.rb:185-224`, `lib/openai/helpers/realtime/transports/async_websocket.rb:294-378` |
@@ -8,6 +8,11 @@ require_relative "../../lib/openai"
8
8
  module OpenAI
9
9
  module Examples
10
10
  module ResponsesWebSocketWorkflows
11
+ # Identify only this example's idle timeout. Other cancellation and
12
+ # transport errors keep their original contracts.
13
+ class PreviewTimeout < StandardError
14
+ end
15
+
11
16
  module_function
12
17
 
13
18
  # One reader drains all requested lanes. This example fails the entire
@@ -80,6 +85,70 @@ module OpenAI
80
85
  )
81
86
  end
82
87
 
88
+ # max_bytes counts cumulative encoded typed events, not wire bytes or
89
+ # total process memory. Call separately for each response on each lane.
90
+ def preview_lane(lane, max_bytes:, seconds:)
91
+ unless max_bytes.is_a?(Integer) &&
92
+ max_bytes.positive? &&
93
+ (seconds.is_a?(Integer) || seconds.is_a?(Float)) &&
94
+ Float(seconds).finite? &&
95
+ seconds.positive?
96
+ raise ArgumentError, "Choose a positive integer byte budget and finite positive lifetime."
97
+ end
98
+
99
+ preview = OpenAI::Responses::IncrementalResponse.new
100
+ admitted_bytes = 0
101
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds
102
+ Async::Task.current.with_timeout(seconds, PreviewTimeout) do
103
+ loop do
104
+ return {outcome: :timeout} if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
105
+
106
+ event = lane.receive
107
+ data = event.is_a?(OpenAI::Responses::UnknownServerEvent) ? event.to_h : event.deep_to_h
108
+ encoded_bytes = JSON.generate(data, max_nesting: false).bytesize
109
+ # Queued reads and JSON encoding can complete without yielding.
110
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
111
+ return {outcome: :timeout, last_received_event: event}
112
+ end
113
+
114
+ if encoded_bytes > max_bytes - admitted_bytes
115
+ return {outcome: :budget_exceeded, last_received_event: event}
116
+ end
117
+
118
+ admitted_bytes += encoded_bytes
119
+ preview.add(event)
120
+ if preview.phase == :terminal
121
+ terminal = preview.terminal_event
122
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
123
+ return {outcome: :timeout, last_received_event: event}
124
+ end
125
+
126
+ return {outcome: :terminal, terminal_event: terminal}
127
+ end
128
+ end
129
+ end
130
+
131
+ rescue PreviewTimeout
132
+ {outcome: :timeout}
133
+ ensure
134
+ preview&.reset
135
+ end
136
+
137
+ def preview(client:, model:)
138
+ # Limits for this short text example only. Choose larger budgets for
139
+ # legitimate image, audio, tool, or other large outputs.
140
+ OpenAI::Responses::Session
141
+ .open(
142
+ client: client,
143
+ limits: session_limits,
144
+ transport_options: {max_message_bytes: 16 * 1024 * 1024}
145
+ ) do |session|
146
+ lane = session.lane("preview")
147
+ lane.send_event(type: "response.create", model: model, input: "Write one brief sentence.")
148
+ preview_lane(lane, max_bytes: 4 * 1024 * 1024, seconds: 15)
149
+ end
150
+ end
151
+
83
152
  def multiplex(client:, model:)
84
153
  OpenAI::Responses::Session.open(client: client, limits: session_limits) do |session|
85
154
  lanes = %w[planner critic].to_h { |name| [name, session.lane(name)] }
@@ -181,7 +250,7 @@ module OpenAI
181
250
  end
182
251
 
183
252
  def run(client:, model:, workflow:, timeout: 60, output: $stdout)
184
- Sync do |task|
253
+ result = Sync do |task|
185
254
  task.with_timeout(timeout) do
186
255
  case workflow
187
256
  when "tools"
@@ -192,13 +261,19 @@ module OpenAI
192
261
  reconnect(client: client, model: model, store: true)
193
262
  when "stateless"
194
263
  reconnect(client: client, model: model, store: false)
264
+ when "preview"
265
+ preview(client: client, model: model)
195
266
  else
196
- raise ArgumentError, "Choose tools, multiplex, stored, or stateless."
267
+ raise ArgumentError, "Choose tools, multiplex, stored, stateless, or preview."
197
268
  end
198
269
  end
199
270
  end
200
271
 
201
- output.puts("Responses WebSocket workflow completed.")
272
+ if workflow == "preview"
273
+ output.puts("Responses WebSocket preview: #{result.fetch(:outcome)}.")
274
+ else
275
+ output.puts("Responses WebSocket workflow completed.")
276
+ end
202
277
  end
203
278
  end
204
279
  end
@@ -101,7 +101,7 @@ module OpenAI
101
101
 
102
102
  [:content, :refusal].each do |field|
103
103
  value = message[field]
104
- unless message.to_h.key?(field) && (value.nil? || value.is_a?(String))
104
+ unless value.nil? || value.is_a?(String)
105
105
  raise Error, "Expected assistant content and refusal to be strings or null"
106
106
  end
107
107
  end
@@ -44,16 +44,46 @@ module OpenAI
44
44
 
45
45
  # A transport-open primary connection. Startup is owned by the caller:
46
46
  # send session.start and observe session.started before further commands.
47
+ # Only one receive call may read or decode at a time. Iteration releases
48
+ # the read before yielding an event to the caller.
47
49
  class Connection < OpenAI::WebSocket::TypedConnection
48
50
  # @api private
49
51
  def initialize(socket:, url:)
50
52
  super(socket: socket, url: url, client_event: client_event_type, server_event: server_event_type)
51
53
  @poisoned = false
52
54
  @closed = false
55
+ @read_mutex = Mutex.new
53
56
  end
54
57
 
55
58
  private :parse_event
56
59
 
60
+ def receive
61
+ with_read_lease { read_one }
62
+ end
63
+
64
+ # @api private
65
+ def receive_raw
66
+ with_read_lease { read_raw_message }
67
+ end
68
+
69
+ private def read_one
70
+ data = read_raw_message
71
+ return nil if data.nil?
72
+
73
+ parse_event(data)
74
+ end
75
+
76
+ private def with_read_lease
77
+ acquired = @read_mutex.try_lock
78
+ unless acquired
79
+ raise connection_error("Live WebSocket already has an active reader."), cause: nil
80
+ end
81
+
82
+ yield
83
+ ensure
84
+ @read_mutex.unlock if acquired
85
+ end
86
+
57
87
  private def coerce_client_event(payload) = coerce_event(@client_event, payload, outbound: true)
58
88
 
59
89
  private def coerce(type, payload)
@@ -210,6 +240,7 @@ module OpenAI
210
240
 
211
241
  OpenAI::WebSocket::ConnectionManager
212
242
  .new(
243
+ client: @client,
213
244
  transport: transport,
214
245
  transport_options: transport_options,
215
246
  default_transport: default_transport,
@@ -22,6 +22,7 @@ module OpenAI
22
22
  def initialize(socket:, url:, recovery: nil)
23
23
  super(socket: socket, url: url)
24
24
  @recovery = recovery
25
+ @read_mutex = Mutex.new
25
26
  @server_event_names = discriminator_values(OpenAI::Realtime::RealtimeServerEvent)
26
27
  @client_event_names = discriminator_values(OpenAI::Realtime::RealtimeClientEvent)
27
28
  @session = OpenAI::Realtime::ConnectionResources::Session.new(self)
@@ -63,6 +64,27 @@ module OpenAI
63
64
  send_raw(encode_client_event(event))
64
65
  end
65
66
 
67
+ # @api private
68
+ def enable_no_status_recovery
69
+ @socket.enable_no_status_recovery if @socket.respond_to?(:enable_no_status_recovery)
70
+ nil
71
+ end
72
+
73
+ private def read_raw_message
74
+ # Recovery already queues callers behind its one physical reader.
75
+ return super if @recovery
76
+
77
+ unless @read_mutex.try_lock
78
+ raise connection_error("A Realtime WebSocket read is already in progress."), cause: nil
79
+ end
80
+
81
+ begin
82
+ super
83
+ ensure
84
+ @read_mutex.unlock
85
+ end
86
+ end
87
+
66
88
  private def write_text(text)
67
89
  return super unless @recovery
68
90
 
@@ -114,6 +136,17 @@ module OpenAI
114
136
 
115
137
  payload = OpenAI::Internal::Type::Converter.dump(OpenAI::Realtime::RealtimeClientEvent, coerced)
116
138
  validate_discriminator!(payload, @client_event_names, kind: "client")
139
+ if payload[:type].to_s == "conversation.item.create"
140
+ item = payload[:item]
141
+ if item.is_a?(Hash)
142
+ item = item.transform_keys(&:to_s)
143
+ if item["type"].to_s == "item_reference" ||
144
+ (item["type"].to_s == "message" && item["role"].to_s == "developer")
145
+ raise ArgumentError, "Realtime item references and developer messages are only valid in response input"
146
+ end
147
+ end
148
+ end
149
+
117
150
  generate_json(payload)
118
151
  rescue SystemStackError
119
152
  raise ArgumentError, "Invalid Realtime client event.", cause: nil
@@ -34,6 +34,7 @@ module OpenAI
34
34
  end
35
35
 
36
36
  super(
37
+ client: client,
37
38
  transport: transport,
38
39
  transport_options: transport_options,
39
40
  default_transport: -> { OpenAI::Realtime::Transports::AsyncWebSocket.new },
@@ -30,6 +30,11 @@ module OpenAI
30
30
  OpenAI::Realtime::RealtimeConversationItemUserMessage
31
31
  when "assistant"
32
32
  OpenAI::Realtime::RealtimeConversationItemAssistantMessage
33
+ when "developer"
34
+ # Use the registered model when available. Earlier generated schemas
35
+ # predate this supported role, so preserve just this message as JSON.
36
+ variants.find { _1.fields.dig(:role, :type) === :developer } ||
37
+ OpenAI::Internal::Type::Unknown
33
38
  else
34
39
  raise ArgumentError, "Realtime message item has an unknown role: #{role.inspect}"
35
40
  end
@@ -99,7 +99,8 @@ module OpenAI
99
99
  end
100
100
 
101
101
  def write(text)
102
- return write_once(text) if Fiber.current.equal?(@owner_fiber)
102
+ # Restore writes bypass the mailbox, but reentrant writes join an active flush.
103
+ return write_once(text) if Fiber.current.equal?(@owner_fiber) && !@mutex.synchronize { @flushing }
103
104
 
104
105
  reply = @mutex.synchronize do
105
106
  raise write_error(text), cause: nil if @error || [:closed, :ending].include?(@state)
@@ -124,6 +125,8 @@ module OpenAI
124
125
 
125
126
  def flush_pending
126
127
  if Fiber.current.equal?(@owner_fiber)
128
+ return nil if @mutex.synchronize { @flushing }
129
+
127
130
  raise state_error("Queue flush interrupted; remaining events were not sent."), cause: nil unless flush
128
131
  return nil
129
132
  end
@@ -246,6 +249,7 @@ module OpenAI
246
249
  private def serve(physical)
247
250
  @owner_fiber = Fiber.current
248
251
  @physical = physical
252
+ @physical.enable_no_status_recovery
249
253
  @mutex.synchronize do
250
254
  return if @state == :closed
251
255
 
@@ -138,6 +138,10 @@ module OpenAI
138
138
  )
139
139
  end
140
140
 
141
+ if @active_reads.positive?
142
+ raise connection_error("A Realtime Translation WebSocket read is already in progress."), cause: nil
143
+ end
144
+
141
145
  @active_reads += 1
142
146
  end
143
147
 
@@ -245,6 +249,7 @@ module OpenAI
245
249
 
246
250
  OpenAI::WebSocket::ConnectionManager
247
251
  .new(
252
+ client: @client,
248
253
  transport: transport,
249
254
  transport_options: transport_options,
250
255
  default_transport: default_transport,
@@ -16,6 +16,19 @@ module OpenAI
16
16
 
17
17
  private_constant :ERROR_FACTORY
18
18
 
19
+ # The optional transport discards an empty close frame's status. Restore
20
+ # it only on sockets owned by automatic recovery, after native validation.
21
+ module RecoverNoStatusClose
22
+ def receive_close(frame)
23
+ super
24
+ if frame.payload.empty?
25
+ raise ::Protocol::WebSocket::ClosedError.new("Peer closed without a status", 1005)
26
+ end
27
+ end
28
+ end
29
+
30
+ private_constant :RecoverNoStatusClose
31
+
19
32
  # Compatibility wrapper for the socket class yielded before the shared
20
33
  # transport extraction.
21
34
  #
@@ -24,6 +37,12 @@ module OpenAI
24
37
  def initialize(connection, url:)
25
38
  super(connection, url: url, error_factory: ERROR_FACTORY)
26
39
  end
40
+
41
+ # @api private
42
+ def enable_no_status_recovery
43
+ @connection.extend(RecoverNoStatusClose) if @connection.respond_to?(:receive_close)
44
+ nil
45
+ end
27
46
  end
28
47
 
29
48
  def initialize(&tls_configurator)
@@ -48,11 +48,20 @@ module OpenAI
48
48
  #
49
49
  # @return [nil]
50
50
  def send_event(event)
51
+ send_client_event(event)
52
+ end
53
+
54
+ # @api private
55
+ def send_response_create(event)
56
+ send_client_event(event, response_create: true)
57
+ end
58
+
59
+ private def send_client_event(event, response_create: false)
51
60
  assert_owner!
52
61
  raise connection_error("Cannot send on a closed Responses WebSocket.") if poisoned? || closed?
53
62
 
54
63
  encoded = begin
55
- generate_json(encode_client_event(event))
64
+ generate_json(encode_client_event(event, response_create: response_create))
56
65
  rescue SystemStackError
57
66
  raise OpenAI::Errors::ResponsesClientEventError.new, cause: nil
58
67
  end
@@ -162,9 +171,12 @@ module OpenAI
162
171
  end
163
172
 
164
173
  # @api private
165
- def encode_client_event(event)
166
- validate_event_tree!(event)
167
- payload = if event.is_a?(Hash)
174
+ def encode_client_event(event, response_create: false)
175
+ create_type = OpenAI::Responses::ResponsesClientEvent::ResponseCreate if response_create
176
+ validate_event_tree!(event, target: create_type)
177
+ payload = if response_create
178
+ OpenAI::Internal::Type::Converter.dump(create_type, event)
179
+ elsif event.is_a?(Hash)
168
180
  OpenAI::Internal::Type::Unknown.dump(event, state: {can_retry: true})
169
181
  else
170
182
  OpenAI::Internal::Type::Converter.dump(OpenAI::Responses::ResponsesClientEvent, event)
@@ -172,7 +184,7 @@ module OpenAI
172
184
 
173
185
  raise OpenAI::Errors::ResponsesClientEventError.new unless payload.is_a?(Hash)
174
186
 
175
- normalize_event_keys(payload)
187
+ OpenAI::Helpers::StructuredOutput.prepare_configuration_updates!(normalize_event_keys(payload))
176
188
  rescue OpenAI::Errors::ResponsesClientEventError
177
189
  raise
178
190
  rescue StandardError
@@ -216,42 +228,68 @@ module OpenAI
216
228
  raise connection_error("Responses WebSocket connections are single-owner.")
217
229
  end
218
230
 
219
- private def validate_event_tree!(value)
231
+ private def validate_event_tree!(value, target: nil)
220
232
  active = {}.compare_by_identity
221
- pending = [[value, false]]
233
+ pending = [[value, false, target]]
222
234
  until pending.empty?
223
- value, exiting = pending.pop
235
+ value, exiting, target = pending.pop
224
236
  if exiting
225
237
  active.delete(value)
226
238
  next
227
239
  end
228
240
 
241
+ # Use the serializer's selected type, so unknown extension hashes keep their
242
+ # wire keys even when they resemble Ruby aliases in another model.
243
+ while target.is_a?(OpenAI::Internal::Type::Union)
244
+ target = target.variant_for_dump(value)
245
+ end
246
+
229
247
  children = case value
230
248
  when OpenAI::Internal::Type::BaseModel
231
249
  data = value.to_h
232
250
  reject_duplicate_serialized_model_keys!(value.class, data)
233
- [data]
251
+ unless target.is_a?(Class) && target <= OpenAI::Internal::Type::BaseModel
252
+ target = value.class
253
+ end
254
+
255
+ [[data, target]]
234
256
  when Hash
235
257
  reject_duplicate_semantic_keys!(value)
236
- value.values
258
+ if target.is_a?(Class) && target <= OpenAI::Internal::Type::BaseModel
259
+ reject_duplicate_serialized_model_keys!(target, value, skip_coerce: true)
260
+ value.map do |key, child|
261
+ name = key.is_a?(String) ? key.to_sym : key
262
+ field = target.known_fields[name]
263
+ field = nil if field && field.fetch(:mode) == :coerce
264
+ [child, field && field.fetch(:type_fn).call]
265
+ end
266
+ elsif target.is_a?(OpenAI::Internal::Type::HashOf)
267
+ item_type = target.item_type
268
+ value.values.map { [_1, item_type] }
269
+ else
270
+ value.values.map { [_1, nil] }
271
+ end
272
+
237
273
  when Array
238
- value
274
+ item_type = target.is_a?(OpenAI::Internal::Type::ArrayOf) ? target.item_type : nil
275
+ value.map { [_1, item_type] }
239
276
  else
240
277
  next
241
278
  end
242
279
 
243
280
  guard_cycle!(value, active)
244
- pending << [value, true]
245
- children.reverse_each { |child| pending << [child, false] }
281
+ pending << [value, true, nil]
282
+ children.reverse_each { |child, child_type| pending << [child, false, child_type] }
246
283
  end
247
284
 
248
285
  nil
249
286
  end
250
287
 
251
- private def reject_duplicate_serialized_model_keys!(model, data)
252
- serialized_keys = data.keys.map do |key|
288
+ private def reject_duplicate_serialized_model_keys!(model, data, skip_coerce: false)
289
+ serialized_keys = data.keys.filter_map do |key|
253
290
  name = key.is_a?(String) ? key.to_sym : key
254
291
  field = model.known_fields[name]
292
+ next if skip_coerce && field && field.fetch(:mode) == :coerce
255
293
  field ? field.fetch(:api_name) : name
256
294
  end
257
295
 
@@ -18,6 +18,7 @@ module OpenAI
18
18
  end
19
19
 
20
20
  super(
21
+ client: client,
21
22
  transport: transport,
22
23
  transport_options: transport_options,
23
24
  default_transport: -> { OpenAI::Responses::Transports::AsyncWebSocket.new },