openai 0.106.0 → 0.107.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -0
  3. data/README.md +1 -1
  4. data/lib/openai/helpers/live/websocket.rb +12 -141
  5. data/lib/openai/helpers/local_audio/process.rb +13 -3
  6. data/lib/openai/helpers/models/embedding.rb +42 -0
  7. data/lib/openai/helpers/realtime/connection.rb +22 -5
  8. data/lib/openai/helpers/realtime/recovery.rb +11 -5
  9. data/lib/openai/helpers/realtime/translation.rb +37 -169
  10. data/lib/openai/helpers/realtime/unknown_server_event.rb +5 -12
  11. data/lib/openai/helpers/responses_websocket/connection.rb +56 -35
  12. data/lib/openai/helpers/responses_websocket/unknown_server_event.rb +2 -14
  13. data/lib/openai/helpers/websocket/async_websocket_transport.rb +8 -6
  14. data/lib/openai/helpers/websocket/client_request.rb +1 -1
  15. data/lib/openai/helpers/websocket/connection.rb +21 -10
  16. data/lib/openai/helpers/websocket/connection_manager.rb +2 -1
  17. data/lib/openai/helpers/websocket/protocol.rb +160 -0
  18. data/lib/openai/helpers/websocket/typed_connection.rb +22 -16
  19. data/lib/openai/helpers/websocket/typed_resource.rb +15 -0
  20. data/lib/openai/internal/transport/base_client.rb +1 -1
  21. data/lib/openai/models/admin/organization/usage_audio_speeches_response.rb +12 -1
  22. data/lib/openai/models/admin/organization/usage_audio_transcriptions_response.rb +12 -1
  23. data/lib/openai/models/admin/organization/usage_code_interpreter_sessions_response.rb +12 -1
  24. data/lib/openai/models/admin/organization/usage_completions_response.rb +12 -1
  25. data/lib/openai/models/admin/organization/usage_costs_response.rb +12 -1
  26. data/lib/openai/models/admin/organization/usage_decisions_response.rb +12 -1
  27. data/lib/openai/models/admin/organization/usage_embeddings_response.rb +12 -1
  28. data/lib/openai/models/admin/organization/usage_file_search_calls_response.rb +12 -1
  29. data/lib/openai/models/admin/organization/usage_images_response.rb +12 -1
  30. data/lib/openai/models/admin/organization/usage_moderations_response.rb +12 -1
  31. data/lib/openai/models/admin/organization/usage_vector_stores_response.rb +12 -1
  32. data/lib/openai/models/admin/organization/usage_web_search_calls_response.rb +12 -1
  33. data/lib/openai/models/beta/beta_response_input_item.rb +10 -1
  34. data/lib/openai/models/responses/response_input_item.rb +10 -1
  35. data/lib/openai/version.rb +1 -1
  36. data/lib/openai.rb +1 -0
  37. data/rbi/openai/client.rbi +1 -8
  38. data/rbi/openai/helpers/realtime/translation.rbi +0 -5
  39. data/rbi/openai/helpers/realtime/unknown_server_event.rbi +6 -0
  40. data/rbi/openai/helpers/websocket.rbi +44 -6
  41. data/rbi/openai/internal/transport/base_client.rbi +2 -6
  42. data/rbi/openai/models/admin/organization/usage_audio_speeches_response.rbi +12 -0
  43. data/rbi/openai/models/admin/organization/usage_audio_transcriptions_response.rbi +12 -0
  44. data/rbi/openai/models/admin/organization/usage_code_interpreter_sessions_response.rbi +12 -0
  45. data/rbi/openai/models/admin/organization/usage_completions_response.rbi +12 -0
  46. data/rbi/openai/models/admin/organization/usage_costs_response.rbi +12 -0
  47. data/rbi/openai/models/admin/organization/usage_decisions_response.rbi +12 -0
  48. data/rbi/openai/models/admin/organization/usage_embeddings_response.rbi +12 -0
  49. data/rbi/openai/models/admin/organization/usage_file_search_calls_response.rbi +12 -0
  50. data/rbi/openai/models/admin/organization/usage_images_response.rbi +12 -0
  51. data/rbi/openai/models/admin/organization/usage_moderations_response.rbi +12 -0
  52. data/rbi/openai/models/admin/organization/usage_vector_stores_response.rbi +12 -0
  53. data/rbi/openai/models/admin/organization/usage_web_search_calls_response.rbi +12 -0
  54. data/rbi/openai/models/beta/beta_response_input_item.rbi +14 -1
  55. data/rbi/openai/models/responses/response_input_item.rbi +10 -1
  56. data/realtime.md +22 -3
  57. data/sig/openai/client.rbs +1 -1
  58. data/sig/openai/helpers/realtime/translation.rbs +0 -2
  59. data/sig/openai/helpers/realtime/unknown_server_event.rbs +2 -0
  60. data/sig/openai/helpers/websocket.rbs +23 -6
  61. data/sig/openai/models/admin/organization/usage_audio_speeches_response.rbs +10 -0
  62. data/sig/openai/models/admin/organization/usage_audio_transcriptions_response.rbs +10 -0
  63. data/sig/openai/models/admin/organization/usage_code_interpreter_sessions_response.rbs +10 -0
  64. data/sig/openai/models/admin/organization/usage_completions_response.rbs +10 -0
  65. data/sig/openai/models/admin/organization/usage_costs_response.rbs +10 -0
  66. data/sig/openai/models/admin/organization/usage_decisions_response.rbs +10 -0
  67. data/sig/openai/models/admin/organization/usage_embeddings_response.rbs +10 -0
  68. data/sig/openai/models/admin/organization/usage_file_search_calls_response.rbs +10 -0
  69. data/sig/openai/models/admin/organization/usage_images_response.rbs +10 -0
  70. data/sig/openai/models/admin/organization/usage_moderations_response.rbs +10 -0
  71. data/sig/openai/models/admin/organization/usage_vector_stores_response.rbs +10 -0
  72. data/sig/openai/models/admin/organization/usage_web_search_calls_response.rbs +10 -0
  73. data/sig/openai/models/beta/beta_response_input_item.rbs +8 -0
  74. data/sig/openai/models/responses/response_input_item.rbs +8 -3
  75. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c24e00e4168b6f3661b2eb29d8de97727d4bcd6a80e40f68512ab21c3c025b46
4
- data.tar.gz: 16f33bff54375b5f3fb5174af63026f95e79ab2ca1cdfe6dd07cedfacac31568
3
+ metadata.gz: e70580b3709d068f537d9eed65ea7e9da96f89e0e2260b36ebf23a9c3590ecf1
4
+ data.tar.gz: 83910cf81a9410c93588e877cca7f88e3d70cbe07e8eed3cd0789b08e3299a0c
5
5
  SHA512:
6
- metadata.gz: 0fe9484b436e02d3b50981251596b57195cef1a604207b7b496049a2214b44eb3953447568b3dcfb3e1e0827279785587e66dfc1d2427c1fa858b7bcb87c54c1
7
- data.tar.gz: 2cabbb26e7382803fbc4aa5ddcc47e3be1bad88c2d05510fb0a44f537653761a0c888157d407764ebeaab8de174cf5f8a44338ae868b16c016914b31cdd1978c
6
+ metadata.gz: 0e854d18548c800c72da1eb04b022befc61b36ccd3e1d24fa732db7b66a0d5c3fdca08a73dd0be579f97ad2c24fbf94f318c42e800eea5b9a8118b35b18b6cbe
7
+ data.tar.gz: 8dd6d3894a1137351eea2bcdad51308b46cdf025de798e913bc1f64c198beb31a264140dcd1c2d901249a21f0bea4c22a6ea61ed274a54f9f031fef73a457cf3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.107.0](https://github.com/openai/openai-ruby/compare/v0.106.0...v0.107.0) (2026-10-20)
4
+
5
+
6
+ ### Features
7
+
8
+ * **api:** add 30-minute cache-write usage counts ([#915](https://github.com/openai/openai-ruby/issues/915)) ([ea2dd66](https://github.com/openai/openai-ruby/commit/ea2dd66ed0c16e544490332fc2ea8ec4f5abd1bb))
9
+ * **api:** add compaction trigger IDs ([#934](https://github.com/openai/openai-ruby/issues/934)) ([224e04a](https://github.com/openai/openai-ruby/commit/224e04ac2f610c793d772efafe7244d6c88039fc))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **api:** correct authentication security key types ([#938](https://github.com/openai/openai-ruby/issues/938)) ([4a5cc74](https://github.com/openai/openai-ruby/commit/4a5cc744b38dc39042d98ae2ff5a32fdd0247bc9))
15
+ * decode base64 embedding vectors ([#942](https://github.com/openai/openai-ruby/issues/942)) ([d684df4](https://github.com/openai/openai-ruby/commit/d684df4dc2a633e34836a346bbbe1e7cbb3d2707))
16
+ * drain audio diagnostics while waiting on input ([#920](https://github.com/openai/openai-ruby/issues/920)) ([8a0b876](https://github.com/openai/openai-ruby/commit/8a0b87606e55f26e86e289577cdfd2533c329ad6))
17
+ * drain ready audio diagnostics without delay ([#918](https://github.com/openai/openai-ruby/issues/918)) ([3e87ce3](https://github.com/openai/openai-ruby/commit/3e87ce380fafe08bb4348b9b867d4006bb939e49))
18
+ * keep Realtime diagnostic rendering payload-free ([#924](https://github.com/openai/openai-ruby/issues/924)) ([3a29026](https://github.com/openai/openai-ruby/commit/3a29026ab177704dbe30d688fddd085e737dfc6d))
19
+ * preserve deep JSON in WebSocket event codecs ([#928](https://github.com/openai/openai-ruby/issues/928)) ([57462c9](https://github.com/openai/openai-ruby/commit/57462c938e3b7602c456b0816e8873bd67fad241))
20
+ * preserve typed Realtime event identity ([#931](https://github.com/openai/openai-ruby/issues/931)) ([9bb5618](https://github.com/openai/openai-ruby/commit/9bb56187c7eb0d72f992c76fed68369d34972d81))
21
+ * **realtime:** abort connections after failed or canceled WebSocket writes so buffered application data cannot be flushed later. On a poisoned connection, `close(code:, reason:)` aborts without a close handshake; the supplied code and reason are not sent. ([#914](https://github.com/openai/openai-ruby/issues/914)) ([d219f3d](https://github.com/openai/openai-ruby/commit/d219f3d0461dd96911429f1b89369eadd382f234))
22
+ * report terminal Realtime failures before transport cleanup ([#923](https://github.com/openai/openai-ruby/issues/923)) ([0d21d75](https://github.com/openai/openai-ruby/commit/0d21d751cd89621c6a32c0048c45ef6e5b7c1af4))
23
+ * retain Translation completion across callback exits ([#921](https://github.com/openai/openai-ruby/issues/921)) ([e6fb44e](https://github.com/openai/openai-ruby/commit/e6fb44e57d233e657d982eee4cf27c6a5d7977c7))
24
+ * share strict WebSocket model validation ([#929](https://github.com/openai/openai-ruby/issues/929)) ([51c6f0b](https://github.com/openai/openai-ruby/commit/51c6f0b73081f0f02f981d2beb4a8b129b2ec1ba))
25
+ * validate nested WebSocket wire field names ([#922](https://github.com/openai/openai-ruby/issues/922)) ([ae551ef](https://github.com/openai/openai-ruby/commit/ae551ef48d52d2c2f415f63c39e0c643fcfb1d75))
26
+ * validate typed WebSocket event constants before conversion ([#925](https://github.com/openai/openai-ruby/issues/925)) ([3ae0cd3](https://github.com/openai/openai-ruby/commit/3ae0cd3070b4fcf046f8823f4cca1b7b8829effe))
27
+
28
+
29
+ ### Chores
30
+
31
+ * **api:** clarify project API key update requirements ([#936](https://github.com/openai/openai-ruby/issues/936)) ([34606cc](https://github.com/openai/openai-ruby/commit/34606cc46087dae5a643256e9cae93966a106182))
32
+ * **api:** Keep SDK maintenance checks aligned ([#917](https://github.com/openai/openai-ruby/issues/917)) ([ebe5bf4](https://github.com/openai/openai-ruby/commit/ebe5bf4dd74d87acf86e30f5c7a78350ce3f8e8d))
33
+ * **api:** preserve audio transcription response compatibility ([#911](https://github.com/openai/openai-ruby/issues/911)) ([c93fb88](https://github.com/openai/openai-ruby/commit/c93fb88b350c80d724912351cf02d7ff51e3073a))
34
+ * **api:** preserve JSON Schema adapter types ([#930](https://github.com/openai/openai-ruby/issues/930)) ([f1430b8](https://github.com/openai/openai-ruby/commit/f1430b8038c7f01123fbf9f2703f6eb5b7fa8c14))
35
+ * **api:** preserve legacy Threads stream behavior ([#910](https://github.com/openai/openai-ruby/issues/910)) ([436887f](https://github.com/openai/openai-ruby/commit/436887fbe6bc3d0874cb75dca501ba1da0a9d9b4))
36
+ * **api:** preserve optional Decisions usage filters ([#909](https://github.com/openai/openai-ruby/issues/909)) ([34f61df](https://github.com/openai/openai-ruby/commit/34f61dfc23bd1ade311e09a036ef6696af37c138))
37
+ * **api:** preserve optional image edit safety identifiers ([#913](https://github.com/openai/openai-ruby/issues/913)) ([6bbfad1](https://github.com/openai/openai-ruby/commit/6bbfad1a6305b5efc71dbb41dce8560d2865bda2))
38
+ * **api:** preserve standard library loading ([#935](https://github.com/openai/openai-ruby/issues/935)) ([a742640](https://github.com/openai/openai-ruby/commit/a7426400cf499f0d88c9e88e694d86369f8d9cb5))
39
+ * **deps-dev:** bump the development-dependencies group with 3 updates ([#912](https://github.com/openai/openai-ruby/issues/912)) ([1227860](https://github.com/openai/openai-ruby/commit/1227860daec638117ce1f14dcd570cffc92c9647))
40
+ * **deps:** bump ruby/setup-ruby from 1.329.0 to 1.331.0 ([#937](https://github.com/openai/openai-ruby/issues/937)) ([8b3ab95](https://github.com/openai/openai-ruby/commit/8b3ab95e7332a7c5325b4718d4e01246c5a54054))
41
+ * prepare generic WebSocket binding signatures ([#941](https://github.com/openai/openai-ruby/issues/941)) ([9206c8c](https://github.com/openai/openai-ruby/commit/9206c8ceb99cc18df859d06c251fa5336c59ecdf))
42
+
43
+
44
+ ### Documentation
45
+
46
+ * clarify Realtime guide scope and Translation support ([#905](https://github.com/openai/openai-ruby/issues/905)) ([efb4004](https://github.com/openai/openai-ruby/commit/efb4004aa49a8a4374833f4b07fbd373fdebb685))
47
+
48
+
49
+ ### Refactors
50
+
51
+ * share Live codecs and use generated sideband commands ([#933](https://github.com/openai/openai-ruby/issues/933)) ([02861d0](https://github.com/openai/openai-ruby/commit/02861d0780eb18f493f881cfa9ed7cdf9a68abe3))
52
+ * share Translation and binding event codecs ([#932](https://github.com/openai/openai-ruby/issues/932)) ([7dc0088](https://github.com/openai/openai-ruby/commit/7dc00884ba30a54a965fc41e8b19e1840e9faec9))
53
+
3
54
  ## [0.106.0](https://github.com/openai/openai-ruby/compare/v0.105.0...v0.106.0) (2026-10-17)
4
55
 
5
56
 
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.106.0"
18
+ gem "openai", "~> 0.107.0"
19
19
  ```
20
20
 
21
21
  <!-- x-release-please-end -->
@@ -26,6 +26,8 @@ module OpenAI
26
26
  # A future server event retains its raw data, but routine diagnostics do not
27
27
  # render potentially sensitive audio, transcript, or session fields.
28
28
  class UnknownServerEvent
29
+ include OpenAI::WebSocket::Protocol
30
+
29
31
  attr_reader :type, :data
30
32
 
31
33
  def initialize(data:)
@@ -38,160 +40,33 @@ module OpenAI
38
40
  def inspect = "#<#{self.class} type=#{@type.inspect}>"
39
41
  alias to_s inspect
40
42
 
41
- private def freeze_json(value)
42
- pending = [value]
43
- visited = {}.compare_by_identity
44
- until pending.empty?
45
- item = pending.pop
46
- next if visited.key?(item)
47
- visited[item] = true
48
- case item
49
- when Hash
50
- item.each { |key, val| pending.push(key, val) }
51
- when Array
52
- pending.concat(item)
53
- end
54
-
55
- item.freeze
56
- end
57
-
58
- value
59
- end
60
43
  end
61
44
 
62
45
  # A transport-open primary connection. Startup is owned by the caller:
63
46
  # send session.start and observe session.started before further commands.
64
- class Connection < OpenAI::WebSocket::Connection
65
- include OpenAI::WebSocket::Protocol
66
- include OpenAI::WebSocket::GuardedConnection
67
-
47
+ class Connection < OpenAI::WebSocket::TypedConnection
68
48
  # @api private
69
49
  def initialize(socket:, url:)
70
- super
50
+ super(socket: socket, url: url, client_event: client_event_type, server_event: server_event_type)
71
51
  @poisoned = false
72
52
  @closed = false
73
- @server_event_names = discriminator_values(server_event_type)
74
- @client_event_names = discriminator_values(client_event_type)
75
- end
76
-
77
- def send_event(event)
78
- send_raw(encode_client_event(event))
79
53
  end
80
54
 
81
- private def encode_client_event(event)
82
- validate_event_tree!(event)
83
- payload = OpenAI::Internal::Type::Converter.dump(client_event_type, event)
84
- type = payload[:type] || payload["type"] if payload.is_a?(Hash)
85
- raise ArgumentError unless (type.is_a?(String) || type.is_a?(Symbol)) && @client_event_names.key?(type.to_s)
86
-
87
- coerced = coerce_event(client_event_type, payload, outbound: true)
88
- serialized = OpenAI::Internal::Type::Converter.dump(client_event_type, coerced)
89
- validate_event_tree!(serialized, json_only: true)
90
- JSON.generate(serialized, max_nesting: false)
91
- rescue StandardError, SystemStackError
92
- raise ArgumentError, "Invalid Live client event.", cause: nil
93
- end
55
+ private :parse_event
94
56
 
95
- private def parse_event(data)
96
- parsed = JSON.parse(data, symbolize_names: true, max_nesting: false)
97
- type = event_type(parsed, message: "Live server event must be an object with a string type")
98
- unless @server_event_names.key?(type.to_s)
99
- return OpenAI::Live::UnknownServerEvent.new(data: parsed)
100
- end
57
+ private def coerce_client_event(payload) = coerce_event(@client_event, payload, outbound: true)
101
58
 
102
- event = coerce_event(server_event_type, parsed)
59
+ private def coerce(type, payload)
60
+ event = coerce_event(type, payload)
103
61
  validate_event_tree!(event)
104
62
  event
105
- rescue StandardError, SystemStackError
106
- raise OpenAI::Errors::LiveProtocolError.new, cause: nil
107
63
  end
108
64
 
109
- private def coerce_event(union, payload, outbound: false)
110
- state = OpenAI::Internal::Type::Converter.new_coerce_state(request_only: outbound)
111
- event = OpenAI::Internal::Type::Converter.coerce(union, payload, state: state)
112
- raise ArgumentError if state[:error] || !state.fetch(:exactness).fetch(:no).zero?
113
- pending = [event]
114
- until pending.empty?
115
- value = pending.pop
116
- case value
117
- when OpenAI::Internal::Type::BaseModel
118
- value.class.fields.each do |name, field|
119
- if field.fetch(:required) &&
120
- field.fetch(:mode) != (outbound ? :coerce : :dump) &&
121
- (!outbound || field.fetch(:const) == OpenAI::Internal::OMIT) &&
122
- !value.to_h.key?(name)
123
- raise ArgumentError
124
- end
125
- end
126
-
127
- pending.concat(value.to_h.values)
128
- when Hash
129
- pending.concat(value.values)
130
- when Array
131
- pending.concat(value)
132
- end
133
- end
134
-
135
- event
136
- end
65
+ private def client_event_error_message = "Invalid Live client event."
137
66
 
138
- private def validate_event_tree!(event, json_only: false)
139
- ancestors = {}.compare_by_identity
140
- pending = [[event, false]]
141
- until pending.empty?
142
- value, exiting = pending.pop
143
- if exiting
144
- ancestors.delete(value)
145
- next
146
- end
67
+ private def unknown_server_event(payload) = OpenAI::Live::UnknownServerEvent.new(data: payload)
147
68
 
148
- children = case value
149
- when OpenAI::Internal::Type::BaseModel
150
- raise ArgumentError if json_only
151
- data = value.to_h
152
- keys = {}
153
- data.each do |key, val|
154
- name = key.is_a?(String) ? key.to_sym : key
155
- field = value.class.known_fields[name]
156
- serialized_name = field ? field.fetch(:api_name).to_s : name.to_s
157
- raise ArgumentError if keys.key?(serialized_name)
158
- keys[serialized_name] = true
159
- next unless field
160
- const = field.fetch(:const)
161
- next if const == OpenAI::Internal::OMIT
162
- if const.is_a?(Symbol)
163
- raise ArgumentError unless (val.is_a?(String) || val.is_a?(Symbol)) && val.to_s == const.to_s
164
- elsif val != const
165
- raise ArgumentError
166
- end
167
- end
168
-
169
- [data]
170
- when Hash
171
- keys = {}
172
- value.each_key do |key|
173
- raise ArgumentError unless key.is_a?(String) || key.is_a?(Symbol)
174
- name = key.to_s
175
- raise ArgumentError if keys.key?(name)
176
- keys[name] = true
177
- end
178
-
179
- value.values
180
- when Array
181
- value
182
- when NilClass, TrueClass, FalseClass, String, Integer, Float, Symbol
183
- next
184
- else
185
- raise ArgumentError if json_only
186
- next
187
- end
188
-
189
- raise ArgumentError if ancestors.key?(value)
190
- ancestors[value] = true
191
- pending << [value, true]
192
- children.each { |child| pending << [child, false] }
193
- end
194
- end
69
+ private def protocol_error = OpenAI::Errors::LiveProtocolError.new
195
70
 
196
71
  private def transport_error = OpenAI::Errors::LiveConnectionError.new(url: @url)
197
72
  private def failed_read_message = "Cannot read from a failed Live WebSocket."
@@ -206,11 +81,7 @@ module OpenAI
206
81
 
207
82
  # Attached to an existing session; no new session.start handshake is sent.
208
83
  class SidebandConnection < Connection
209
- # @api private
210
- def initialize(socket:, url:)
211
- super
212
- @client_event_names = @client_event_names.except("session.start", "session.input_audio.append")
213
- end
84
+ private def client_event_type = OpenAI::Live::SidebandClientEvent
214
85
  end
215
86
 
216
87
  # Caller-driven startup using overrides for an eligible stored session.
@@ -70,6 +70,7 @@ module OpenAI
70
70
  if @error_reader && !@error_reader.closed?
71
71
  chunk = @error_reader.read_nonblock(65_536, exception: false)
72
72
  @errors = true if chunk.is_a?(String) && !chunk.empty?
73
+ @error_reader.close if chunk.nil?
73
74
  end
74
75
 
75
76
  !!@errors
@@ -86,7 +87,9 @@ module OpenAI
86
87
  raise PlaybackError, "Audio player exited before the input ended." if @status
87
88
  end
88
89
 
89
- io.wait_readable(0.05)
90
+ readers = [io]
91
+ readers << @error_reader if @error_reader && !@error_reader.closed?
92
+ IO.select(readers, nil, nil, 0.05)
90
93
  end
91
94
  end
92
95
 
@@ -96,7 +99,8 @@ module OpenAI
96
99
  check_deadline
97
100
  result = io.write_nonblock(bytes.byteslice(offset, 65_536), exception: false)
98
101
  if result == :wait_writable
99
- io.wait_writable(0.05)
102
+ readers = @error_reader && !@error_reader.closed? ? [@error_reader] : []
103
+ IO.select(readers, [io], nil, 0.05)
100
104
  else
101
105
  offset += result
102
106
  end
@@ -110,7 +114,13 @@ module OpenAI
110
114
  until @status
111
115
  check_deadline
112
116
  reap
113
- sleep(0.01) unless @status
117
+ unless @status
118
+ if @error_reader && !@error_reader.closed?
119
+ @error_reader.wait_readable(0.01)
120
+ else
121
+ sleep(0.01)
122
+ end
123
+ end
114
124
  end
115
125
 
116
126
  @status
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../models/embedding"
4
+
5
+ module OpenAI
6
+ module Models
7
+ class Embedding
8
+ alias_method :embedding_without_base64, :embedding
9
+ private :embedding_without_base64
10
+
11
+ def embedding
12
+ value = self[:embedding]
13
+ return embedding_without_base64 unless value.is_a?(String)
14
+
15
+ # The generated setter replaces this per-field state on assignment.
16
+ cached = @coerced[:embedding]
17
+ return cached if cached.is_a?(Array)
18
+
19
+ # Base64 embedding values contain little-endian float32 samples.
20
+ decoded = begin
21
+ value.unpack1("m0")
22
+ rescue ArgumentError
23
+ nil
24
+ end
25
+
26
+ return embedding_without_base64 unless decoded && (decoded.bytesize % 4).zero?
27
+
28
+ @coerced[:embedding] = decoded.unpack("e*")
29
+ end
30
+
31
+ # Equal embeddings still have equal raw data, so the inherited hash remains valid.
32
+ def ==(other)
33
+ return false unless super
34
+ return true unless self[:embedding].is_a?(String)
35
+
36
+ embedding == other.embedding
37
+ rescue OpenAI::Errors::ConversionError
38
+ true
39
+ end
40
+ end
41
+ end
42
+ end
@@ -33,7 +33,7 @@ module OpenAI
33
33
  # Parse raw JSON as a typed server event. Valid events that are newer than this
34
34
  # SDK remain observable as {UnknownServerEvent} values.
35
35
  def parse_event(data)
36
- parsed = JSON.parse(data, symbolize_names: true)
36
+ parsed = parse_json(data)
37
37
  type = event_type(parsed)
38
38
  unless @server_event_names.key?(type.to_s)
39
39
  return OpenAI::Realtime::UnknownServerEvent.new(data: parsed)
@@ -46,14 +46,16 @@ module OpenAI
46
46
  state: state
47
47
  )
48
48
  if (cause = coercion_error(state))
49
- raise OpenAI::Errors::RealtimeProtocolError.new(data: data, cause: cause)
49
+ raise OpenAI::Errors::RealtimeProtocolError.new(data: data, cause: cause), cause: nil
50
50
  end
51
51
 
52
- event
52
+ validate_wire_field_names!(event, parsed)
53
53
  rescue OpenAI::Errors::RealtimeProtocolError
54
54
  raise
55
+ rescue SystemStackError
56
+ raise OpenAI::Errors::RealtimeProtocolError.new(data: data), cause: nil
55
57
  rescue StandardError => e
56
- raise OpenAI::Errors::RealtimeProtocolError.new(data: data, cause: e)
58
+ raise OpenAI::Errors::RealtimeProtocolError.new(data: data, cause: e), cause: nil
57
59
  end
58
60
 
59
61
  # Validate, encode, and send a typed client event.
@@ -61,6 +63,15 @@ module OpenAI
61
63
  send_raw(encode_client_event(event))
62
64
  end
63
65
 
66
+ private def write_text(text)
67
+ return super unless @recovery
68
+
69
+ # Recovery owns admission and uncertain sends; rejecting a full queue
70
+ # has not attempted a physical write and must leave this connection usable.
71
+ @recovery.write(text)
72
+ nil
73
+ end
74
+
64
75
  # Whether automatic recovery is opening or preparing a replacement socket.
65
76
  def reconnecting? = @recovery ? @recovery.reconnecting? : false
66
77
 
@@ -83,6 +94,10 @@ module OpenAI
83
94
  validate_discriminator!(event, @client_event_names, kind: "client")
84
95
  end
85
96
 
97
+ # Preserve permissive raw JSON keys while checking original typed models
98
+ # before union conversion can replace their identities or renamed fields.
99
+ validate_event_tree!(event, strict_keys: false)
100
+
86
101
  normalized = OpenAI::Internal::Type::Converter.dump(
87
102
  OpenAI::Realtime::RealtimeClientEvent,
88
103
  event
@@ -99,7 +114,9 @@ module OpenAI
99
114
 
100
115
  payload = OpenAI::Internal::Type::Converter.dump(OpenAI::Realtime::RealtimeClientEvent, coerced)
101
116
  validate_discriminator!(payload, @client_event_names, kind: "client")
102
- JSON.generate(payload)
117
+ generate_json(payload)
118
+ rescue SystemStackError
119
+ raise ArgumentError, "Invalid Realtime client event.", cause: nil
103
120
  rescue StandardError => e
104
121
  raise ArgumentError.new("Invalid Realtime client event."), cause: e
105
122
  end
@@ -76,7 +76,7 @@ module OpenAI
76
76
 
77
77
  def take_pending_messages
78
78
  @mutex.synchronize do
79
- raise state_error("Cannot remove queued events while flushing.") if @flushing
79
+ raise state_error("Cannot remove queued events while flushing."), cause: nil if @flushing
80
80
 
81
81
  messages = @pending
82
82
  @pending = []
@@ -106,7 +106,7 @@ module OpenAI
106
106
 
107
107
  if @state != :open || @flushing
108
108
  if @max_queue_bytes.zero? || @pending_bytes + [text.bytesize, 1].max > @max_queue_bytes
109
- raise OpenAI::Errors::RealtimeQueueFullError
109
+ raise OpenAI::Errors::RealtimeQueueFullError, cause: nil
110
110
  end
111
111
 
112
112
  @pending << text.dup.freeze
@@ -124,13 +124,15 @@ module OpenAI
124
124
 
125
125
  def flush_pending
126
126
  if Fiber.current.equal?(@owner_fiber)
127
- raise state_error("Queue flush interrupted; remaining events were not sent.") unless flush
127
+ raise state_error("Queue flush interrupted; remaining events were not sent."), cause: nil unless flush
128
128
  return nil
129
129
  end
130
130
 
131
131
  reply = @mutex.synchronize do
132
132
  raise @error, cause: nil if @error
133
- raise state_error("Cannot flush until the replacement connection is ready.") unless @state == :open
133
+ unless @state == :open
134
+ raise state_error("Cannot flush until the replacement connection is ready."), cause: nil
135
+ end
134
136
 
135
137
  register(:flush).tap { @commands << [:flush, nil, _1, @generation] }
136
138
  end
@@ -348,7 +350,11 @@ module OpenAI
348
350
  uncertain_message: @inflight
349
351
  )
350
352
  elsif @state != :closed
351
- @state = :recovering
353
+ if recoverable?(e) && @attempts < @max_attempts
354
+ @state = :recovering
355
+ else
356
+ fail_locked("Realtime reconnect stopped; the connection could not be recovered.")
357
+ end
352
358
  end
353
359
  end
354
360