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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/README.md +17 -2
- data/docs/architecture/security-model.md +11 -16
- data/examples/responses/websocket_workflows.rb +78 -3
- data/lib/openai/helpers/chat/tool_runner.rb +1 -1
- data/lib/openai/helpers/live/websocket.rb +31 -0
- data/lib/openai/helpers/realtime/connection.rb +33 -0
- data/lib/openai/helpers/realtime/connection_manager.rb +1 -0
- data/lib/openai/helpers/realtime/conversation_item_variant_resolver.rb +5 -0
- data/lib/openai/helpers/realtime/recovery.rb +5 -1
- data/lib/openai/helpers/realtime/translation.rb +5 -0
- data/lib/openai/helpers/realtime/transports/async_websocket.rb +19 -0
- data/lib/openai/helpers/responses_websocket/connection.rb +53 -15
- data/lib/openai/helpers/responses_websocket/connection_manager.rb +1 -0
- data/lib/openai/helpers/responses_websocket/connection_resources.rb +1 -1
- data/lib/openai/helpers/structured_output/response_parser.rb +1 -0
- data/lib/openai/helpers/structured_output.rb +58 -0
- data/lib/openai/helpers/websocket/connection_manager.rb +11 -0
- data/lib/openai/helpers/websocket/resource.rb +1 -0
- data/lib/openai/helpers/websocket/typed_connection.rb +1 -0
- data/lib/openai/internal/type/array_of.rb +1 -1
- data/lib/openai/internal/type/hash_of.rb +1 -1
- data/lib/openai/internal/type/request_parameters.rb +2 -1
- data/lib/openai/internal/type/union.rb +18 -6
- data/lib/openai/models/admin/organization/external_key.rb +169 -0
- data/lib/openai/models/admin/organization/external_key_create_params.rb +311 -0
- data/lib/openai/models/admin/organization/external_key_delete_params.rb +24 -0
- data/lib/openai/models/admin/organization/external_key_list_params.rb +69 -0
- data/lib/openai/models/admin/organization/external_key_retrieve_params.rb +24 -0
- data/lib/openai/models/admin/organization/external_key_validate_params.rb +24 -0
- data/lib/openai/models/admin/organization/external_key_validate_response.rb +20 -0
- data/lib/openai/models/audio/speech_model.rb +12 -0
- data/lib/openai/models/audio_model.rb +15 -0
- data/lib/openai/models/beta/agent.rb +49 -1
- data/lib/openai/models/beta/agent_create_params.rb +51 -1
- data/lib/openai/models/beta/agent_session.rb +75 -2
- data/lib/openai/models/beta/agent_update_params.rb +62 -1
- data/lib/openai/models/beta/agents/session_create_params.rb +94 -2
- data/lib/openai/models/beta/agents/session_list_params.rb +16 -1
- data/lib/openai/models/beta/agents/session_update_params.rb +50 -5
- data/lib/openai/models/beta/assistant_update_params.rb +18 -0
- data/lib/openai/models/beta/beta_response.rb +224 -1
- data/lib/openai/models/beta/beta_response_configuration_update_item.rb +64 -3
- data/lib/openai/models/beta/beta_response_configuration_update_item_param.rb +64 -4
- data/lib/openai/models/beta/beta_response_input_item.rb +2 -3
- data/lib/openai/models/beta/beta_response_item.rb +2 -2
- data/lib/openai/models/beta/beta_response_tool_search_output_item_param.rb +6 -6
- data/lib/openai/models/beta/beta_responses_client_event.rb +20 -1
- data/lib/openai/models/beta/beta_tool.rb +6 -6
- data/lib/openai/models/beta/response_compact_params.rb +6 -0
- data/lib/openai/models/beta/response_create_params.rb +6 -0
- data/lib/openai/models/chat_model.rb +6 -0
- data/lib/openai/models/conversations/conversation_item.rb +2 -2
- data/lib/openai/models/image_edit_params.rb +46 -25
- data/lib/openai/models/live/fork_server_event.rb +43 -3
- data/lib/openai/models/live/server_event.rb +43 -3
- data/lib/openai/models/live/sideband_client_event.rb +40 -1
- data/lib/openai/models/live/sideband_server_event.rb +43 -3
- data/lib/openai/models/realtime/audio_transcription.rb +15 -0
- data/lib/openai/models/realtime/conversation_item_added.rb +2 -2
- data/lib/openai/models/realtime/conversation_item_create_event.rb +2 -2
- data/lib/openai/models/realtime/conversation_item_created_event.rb +2 -2
- data/lib/openai/models/realtime/conversation_item_done.rb +2 -2
- data/lib/openai/models/realtime/realtime_response.rb +2 -2
- data/lib/openai/models/realtime/realtime_response_create_params.rb +2 -2
- data/lib/openai/models/realtime/realtime_server_event.rb +2 -2
- data/lib/openai/models/realtime/realtime_session.rb +18 -0
- data/lib/openai/models/realtime/realtime_session_create_request.rb +18 -0
- data/lib/openai/models/realtime/realtime_session_create_response.rb +18 -0
- data/lib/openai/models/realtime/response_output_item_added_event.rb +2 -2
- data/lib/openai/models/realtime/response_output_item_done_event.rb +2 -2
- data/lib/openai/models/responses/response.rb +221 -1
- data/lib/openai/models/responses/response_compact_params.rb +6 -0
- data/lib/openai/models/responses/response_configuration_update_item.rb +50 -3
- data/lib/openai/models/responses/response_configuration_update_item_param.rb +50 -4
- data/lib/openai/models/responses/response_input_item.rb +2 -3
- data/lib/openai/models/responses/response_item.rb +2 -2
- data/lib/openai/models/responses/response_tool_search_output_item_param.rb +6 -6
- data/lib/openai/models/responses/responses_client_event.rb +14 -1
- data/lib/openai/models/responses/tool.rb +6 -6
- data/lib/openai/net_http_client.rb +6 -0
- data/lib/openai/providers/vertex.rb +0 -10
- data/lib/openai/resources/admin/organization/external_keys.rb +132 -0
- data/lib/openai/resources/admin/organization.rb +4 -0
- data/lib/openai/resources/audio/speech.rb +8 -0
- data/lib/openai/resources/audio/translations.rb +8 -0
- data/lib/openai/resources/beta/agents/sessions.rb +22 -6
- data/lib/openai/resources/beta/agents.rb +17 -2
- data/lib/openai/resources/images.rb +36 -18
- data/lib/openai/version.rb +1 -1
- data/lib/openai.rb +8 -0
- data/rbi/openai/helpers/live/websocket.rbi +1 -12
- data/rbi/openai/helpers/realtime/connection.rbi +5 -0
- data/rbi/openai/helpers/realtime/transports/async_websocket.rbi +5 -0
- data/rbi/openai/helpers/responses_websocket/connection.rbi +7 -2
- data/rbi/openai/helpers/structured_output.rbi +10 -0
- data/rbi/openai/internal/type/array_of.rbi +1 -1
- data/rbi/openai/internal/type/hash_of.rbi +1 -1
- data/rbi/openai/internal/type/union.rbi +5 -0
- data/rbi/openai/models/admin/organization/external_key.rbi +301 -0
- data/rbi/openai/models/admin/organization/external_key_create_params.rbi +532 -0
- data/rbi/openai/models/admin/organization/external_key_delete_params.rbi +57 -0
- data/rbi/openai/models/admin/organization/external_key_list_params.rbi +129 -0
- data/rbi/openai/models/admin/organization/external_key_retrieve_params.rbi +57 -0
- data/rbi/openai/models/admin/organization/external_key_validate_params.rbi +57 -0
- data/rbi/openai/models/admin/organization/external_key_validate_response.rbi +50 -0
- data/rbi/openai/models/beta/agent.rbi +103 -1
- data/rbi/openai/models/beta/agent_create_params.rbi +109 -0
- data/rbi/openai/models/beta/agent_session.rbi +136 -3
- data/rbi/openai/models/beta/agent_update_params.rbi +120 -0
- data/rbi/openai/models/beta/agents/session_create_params.rbi +153 -0
- data/rbi/openai/models/beta/agents/session_list_params.rbi +19 -0
- data/rbi/openai/models/beta/agents/session_update_params.rbi +94 -6
- data/rbi/openai/models/beta/beta_response.rbi +394 -0
- data/rbi/openai/models/beta/beta_response_configuration_update_item.rbi +113 -3
- data/rbi/openai/models/beta/beta_response_configuration_update_item_param.rbi +125 -4
- data/rbi/openai/models/beta/beta_response_tool_search_output_item_param.rbi +6 -6
- data/rbi/openai/models/beta/beta_responses_client_event.rbi +17 -0
- data/rbi/openai/models/beta/beta_tool.rbi +6 -6
- data/rbi/openai/models/decision_create_params.rbi +4 -1
- data/rbi/openai/models/image_edit_params.rbi +49 -24
- data/rbi/openai/models/live/fork_server_event.rbi +65 -1
- data/rbi/openai/models/live/server_event.rbi +65 -1
- data/rbi/openai/models/live/sideband_client_event.rbi +60 -0
- data/rbi/openai/models/live/sideband_server_event.rbi +65 -1
- data/rbi/openai/models/realtime/conversation_item_added.rbi +6 -3
- data/rbi/openai/models/realtime/conversation_item_create_event.rbi +6 -3
- data/rbi/openai/models/realtime/conversation_item_created_event.rbi +6 -3
- data/rbi/openai/models/realtime/conversation_item_done.rbi +6 -3
- data/rbi/openai/models/realtime/realtime_response.rbi +8 -4
- data/rbi/openai/models/realtime/realtime_response_create_params.rbi +8 -4
- data/rbi/openai/models/realtime/realtime_server_event.rbi +6 -3
- data/rbi/openai/models/realtime/response_output_item_added_event.rbi +6 -3
- data/rbi/openai/models/realtime/response_output_item_done_event.rbi +6 -3
- data/rbi/openai/models/responses/response.rbi +394 -0
- data/rbi/openai/models/responses/response_configuration_update_item.rbi +76 -3
- data/rbi/openai/models/responses/response_configuration_update_item_param.rbi +74 -4
- data/rbi/openai/models/responses/response_tool_search_output_item_param.rbi +6 -6
- data/rbi/openai/models/responses/responses_client_event.rbi +17 -0
- data/rbi/openai/models/responses/tool.rbi +6 -6
- data/rbi/openai/net_http_client.rbi +5 -0
- data/rbi/openai/resources/admin/organization/external_keys.rbi +86 -0
- data/rbi/openai/resources/admin/organization.rbi +3 -0
- data/rbi/openai/resources/audio/speech.rbi +8 -0
- data/rbi/openai/resources/audio/translations.rbi +6 -0
- data/rbi/openai/resources/beta/agents/sessions.rbi +18 -2
- data/rbi/openai/resources/beta/agents.rbi +15 -0
- data/rbi/openai/resources/images.rbi +34 -16
- data/rbi/openai/resources/responses.rbi +2 -0
- data/realtime.md +6 -0
- data/responses-websocket.md +53 -0
- data/sig/openai/helpers/live/websocket.rbs +1 -9
- data/sig/openai/helpers/realtime/connection.rbs +3 -0
- data/sig/openai/helpers/realtime/transports/async_websocket.rbs +3 -0
- data/sig/openai/helpers/responses_websocket/connection.rbs +7 -1
- data/sig/openai/helpers/structured_output.rbs +9 -0
- data/sig/openai/internal/type/union.rbs +4 -0
- data/sig/openai/models/admin/organization/external_key.rbs +186 -0
- data/sig/openai/models/admin/organization/external_key_create_params.rbs +423 -0
- data/sig/openai/models/admin/organization/external_key_delete_params.rbs +27 -0
- data/sig/openai/models/admin/organization/external_key_list_params.rbs +81 -0
- data/sig/openai/models/admin/organization/external_key_retrieve_params.rbs +27 -0
- data/sig/openai/models/admin/organization/external_key_validate_params.rbs +27 -0
- data/sig/openai/models/admin/organization/external_key_validate_response.rbs +17 -0
- data/sig/openai/models/admin/organization/external_storage_create_params.rbs +72 -2
- data/sig/openai/models/audio/speech_create_params.rbs +15 -2
- data/sig/openai/models/audio/transcription_create_params.rbs +30 -2
- data/sig/openai/models/audio/voice_create_params.rbs +47 -2
- data/sig/openai/models/beta/agent.rbs +54 -2
- data/sig/openai/models/beta/agent_create_params.rbs +54 -0
- data/sig/openai/models/beta/agent_session.rbs +82 -3
- data/sig/openai/models/beta/agent_update_params.rbs +64 -0
- data/sig/openai/models/beta/agents/session_create_params.rbs +91 -0
- data/sig/openai/models/beta/agents/session_list_params.rbs +20 -0
- data/sig/openai/models/beta/agents/session_update_params.rbs +48 -6
- data/sig/openai/models/beta/assistant_create_params.rbs +5 -2
- data/sig/openai/models/beta/beta_response.rbs +223 -0
- data/sig/openai/models/beta/beta_response_configuration_update_item.rbs +58 -2
- data/sig/openai/models/beta/beta_response_configuration_update_item_param.rbs +58 -2
- data/sig/openai/models/beta/beta_response_tool_search_output_item_param.rbs +4 -4
- data/sig/openai/models/beta/beta_responses_client_event.rbs +17 -0
- data/sig/openai/models/beta/beta_tool.rbs +4 -4
- data/sig/openai/models/beta/response_create_params.rbs +5 -2
- data/sig/openai/models/beta/responses/input_token_count_params.rbs +10 -2
- data/sig/openai/models/beta/thread_create_and_run_params.rbs +11 -5
- data/sig/openai/models/beta/thread_create_params.rbs +11 -5
- data/sig/openai/models/beta/threads/message_create_params.rbs +6 -3
- data/sig/openai/models/beta/threads/run_create_params.rbs +6 -3
- data/sig/openai/models/decision_create_params.rbs +27 -2
- data/sig/openai/models/eval_create_params.rbs +25 -4
- data/sig/openai/models/evals/run_create_params.rbs +52 -8
- data/sig/openai/models/image_edit_params.rbs +47 -16
- data/sig/openai/models/live/fork_server_event.rbs +39 -0
- data/sig/openai/models/live/server_event.rbs +39 -0
- data/sig/openai/models/live/session_accept_params.rbs +39 -2
- data/sig/openai/models/live/sideband_client_event.rbs +32 -0
- data/sig/openai/models/live/sideband_server_event.rbs +39 -0
- data/sig/openai/models/realtime/conversation_item.rbs +1 -0
- data/sig/openai/models/responses/input_token_count_params.rbs +10 -2
- data/sig/openai/models/responses/response.rbs +223 -0
- data/sig/openai/models/responses/response_configuration_update_item.rbs +45 -2
- data/sig/openai/models/responses/response_configuration_update_item_param.rbs +45 -2
- data/sig/openai/models/responses/response_create_params.rbs +5 -2
- data/sig/openai/models/responses/response_tool_search_output_item_param.rbs +4 -4
- data/sig/openai/models/responses/responses_client_event.rbs +17 -0
- data/sig/openai/models/responses/tool.rbs +4 -4
- data/sig/openai/models/video_edit_params.rbs +11 -2
- data/sig/openai/models/video_extend_params.rbs +11 -2
- data/sig/openai/net_http_client.rbs +3 -0
- data/sig/openai/resources/admin/organization/external_keys.rbs +70 -0
- data/sig/openai/resources/admin/organization/external_storage.rbs +23 -1
- data/sig/openai/resources/admin/organization.rbs +2 -0
- data/sig/openai/resources/audio/speech.rbs +2 -1
- data/sig/openai/resources/audio/transcriptions.rbs +14 -2
- data/sig/openai/resources/audio/voices.rbs +14 -1
- data/sig/openai/resources/beta/agents/sessions.rbs +4 -0
- data/sig/openai/resources/beta/agents.rbs +4 -0
- data/sig/openai/resources/beta/responses/input_tokens.rbs +2 -1
- data/sig/openai/resources/beta/responses.rbs +4 -2
- data/sig/openai/resources/decisions.rbs +7 -1
- data/sig/openai/resources/images.rbs +2 -0
- data/sig/openai/resources/responses/input_tokens.rbs +2 -1
- data/sig/openai/resources/responses.rbs +5 -2
- data/sig/openai/resources/videos.rbs +2 -2
- metadata +25 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ced72b8e35330a5d4832f6e8a19c4b989c320faea258dcae427e0df5daa5af7
|
|
4
|
+
data.tar.gz: 346d52cbceace88063f31b32abb7f3e3f0a6c85da9315444335534f5fc3d554c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
267
|
+
raise ArgumentError, "Choose tools, multiplex, stored, stateless, or preview."
|
|
197
268
|
end
|
|
198
269
|
end
|
|
199
270
|
end
|
|
200
271
|
|
|
201
|
-
|
|
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
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|