openai 0.26.0 → 0.27.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3c1a71d808c79d0cc0c3d6258103489ca127f3c437d79823b8975e73a5c5027
4
- data.tar.gz: a5cd6cc4d1cb2d7f144b9d7f498d9ae51d540bb025883b0c04f9ac60c751487e
3
+ metadata.gz: bae9059a58e735637f77e8f67206ae5461c0a9b1644dae426b5ecdc783f9856b
4
+ data.tar.gz: 2b0724ad0cc6348a15db3d6d305cc451bb289ec7d8af9f8375d97fa884be4acc
5
5
  SHA512:
6
- metadata.gz: f923479ccd9602e086b3dd87cb4b17f94cc42146f9e61b313473161f824cba737b4770c776bac4158e658896d8f77a2c1fcff571c99e97a544da89c8f6fbceb7
7
- data.tar.gz: 5113901c49d3bb0476d7076ff41270e1baa68a02b54db309312589214d372607352936b310a6d8ce6b4ccdb02d72054043c79b1b3c38e560f19e065b091c2867
6
+ metadata.gz: b93a9f4249988394f4d90dd36c1f161eea365978ad6b83d3e9e9ecd8579d10f5f0ac26320a5f698dc2174755553ab0048e33602992c3d213c26e5f97d7b2adf9
7
+ data.tar.gz: dbe13a678f0617a93002ab5f14ca13b2f1305083ac2b5ecc3982d1afa925698dd41673b1bade182d6287e4558177b26e75b465af3d382111d0a640687c30dbb4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.27.0 (2025-09-26)
4
+
5
+ Full Changelog: [v0.26.0...v0.27.0](https://github.com/openai/openai-ruby/compare/v0.26.0...v0.27.0)
6
+
7
+ ### Features
8
+
9
+ * chat completion streaming helpers ([#828](https://github.com/openai/openai-ruby/issues/828)) ([6e98424](https://github.com/openai/openai-ruby/commit/6e9842485e819876dd6b78107fa45f1a5da67e4f))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **internal:** use null byte as file separator in the fast formatting script ([151ffe1](https://github.com/openai/openai-ruby/commit/151ffe10c9dc8d5edaf46de2a1c6b6e6fda80034))
15
+ * shorten multipart boundary sep to less than RFC specificed max length ([d7770d1](https://github.com/openai/openai-ruby/commit/d7770d10ee3b093d8e2464b79e0e12be3a9d2beb))
16
+
17
+
18
+ ### Performance Improvements
19
+
20
+ * faster code formatting ([67da711](https://github.com/openai/openai-ruby/commit/67da71139e5b572c97539299c39bae04c1d569fd))
21
+
22
+
23
+ ### Chores
24
+
25
+ * allow fast-format to use bsd sed as well ([66ac913](https://github.com/openai/openai-ruby/commit/66ac913d195d8b5a5c4474ded88a5f9dad13b7b6))
26
+
3
27
  ## 0.26.0 (2025-09-23)
4
28
 
5
29
  Full Changelog: [v0.25.1...v0.26.0](https://github.com/openai/openai-ruby/compare/v0.25.1...v0.26.0)
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.26.0"
18
+ gem "openai", "~> 0.27.0"
19
19
  ```
20
20
 
21
21
  <!-- x-release-please-end -->
@@ -0,0 +1,683 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenAI
4
+ module Helpers
5
+ module Streaming
6
+ class ChatCompletionStream
7
+ include OpenAI::Internal::Type::BaseStream
8
+
9
+ def initialize(raw_stream:, response_format: nil, input_tools: nil)
10
+ @raw_stream = raw_stream
11
+ @state = ChatCompletionStreamState.new(
12
+ response_format: response_format,
13
+ input_tools: input_tools
14
+ )
15
+ @iterator = iterator
16
+ end
17
+
18
+ def get_final_completion
19
+ until_done
20
+ @state.get_final_completion
21
+ end
22
+
23
+ def get_output_text
24
+ completion = get_final_completion
25
+ text_parts = []
26
+
27
+ completion.choices.each do |choice|
28
+ next unless choice.message.content
29
+ text_parts << choice.message.content
30
+ end
31
+
32
+ text_parts.join
33
+ end
34
+
35
+ def until_done
36
+ each {} # rubocop:disable Lint/EmptyBlock
37
+ self
38
+ end
39
+
40
+ def current_completion_snapshot
41
+ @state.current_completion_snapshot
42
+ end
43
+
44
+ def text
45
+ OpenAI::Internal::Util.chain_fused(@iterator) do |yielder|
46
+ @iterator.each do |event|
47
+ yielder << event.delta if event.is_a?(ChatContentDeltaEvent)
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def iterator
55
+ @iterator ||= OpenAI::Internal::Util.chain_fused(@raw_stream) do |y|
56
+ @raw_stream.each do |raw_event|
57
+ next unless valid_chat_completion_chunk?(raw_event)
58
+ @state.handle_chunk(raw_event).each do |event|
59
+ y << event
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def valid_chat_completion_chunk?(sse_event)
66
+ # Although the _raw_stream is always supposed to contain only objects adhering to ChatCompletionChunk schema,
67
+ # this is broken by the Azure OpenAI in case of Asynchronous Filter enabled.
68
+ # An easy filter is to check for the "object" property:
69
+ # - should be "chat.completion.chunk" for a ChatCompletionChunk;
70
+ # - is an empty string for Asynchronous Filter events.
71
+ sse_event.object == :"chat.completion.chunk"
72
+ end
73
+ end
74
+
75
+ class ChatCompletionStreamState
76
+ attr_reader :current_completion_snapshot
77
+
78
+ def initialize(response_format: nil, input_tools: nil)
79
+ @current_completion_snapshot = nil
80
+ @choice_event_states = []
81
+ @input_tools = Array(input_tools)
82
+ @response_format = response_format
83
+ @rich_response_format = response_format.is_a?(Class) ? response_format : nil
84
+ end
85
+
86
+ def get_final_completion
87
+ parse_chat_completion(
88
+ chat_completion: current_completion_snapshot,
89
+ response_format: @rich_response_format
90
+ )
91
+ end
92
+
93
+ # Transforms raw streaming chunks into higher-level events that represent content changes,
94
+ # tool calls, and completion states. It maintains a running snapshot of the complete
95
+ # response by accumulating data from each chunk.
96
+ #
97
+ # The method performs the following steps:
98
+ # 1. Unwraps the chunk if it's wrapped in a ChatChunkEvent
99
+ # 2. Filters out non-ChatCompletionChunk objects
100
+ # 3. Accumulates the chunk data into the current completion snapshot
101
+ # 4. Generates appropriate events based on the chunk's content
102
+ def handle_chunk(chunk)
103
+ chunk = chunk.chunk if chunk.is_a?(ChatChunkEvent)
104
+
105
+ return [] unless chunk.is_a?(OpenAI::Chat::ChatCompletionChunk)
106
+
107
+ @current_completion_snapshot = accumulate_chunk(chunk)
108
+ build_events(chunk: chunk, completion_snapshot: @current_completion_snapshot)
109
+ end
110
+
111
+ private
112
+
113
+ def get_choice_state(choice)
114
+ index = choice.index
115
+ @choice_event_states[index] ||= ChoiceEventState.new(input_tools: @input_tools)
116
+ end
117
+
118
+ def accumulate_chunk(chunk)
119
+ if @current_completion_snapshot.nil?
120
+ return convert_initial_chunk_into_snapshot(chunk)
121
+ end
122
+
123
+ completion_snapshot = @current_completion_snapshot
124
+
125
+ chunk.choices.each do |choice|
126
+ accumulate_choice!(choice, completion_snapshot)
127
+ end
128
+
129
+ completion_snapshot.usage = chunk.usage if chunk.usage
130
+ completion_snapshot.system_fingerprint = chunk.system_fingerprint if chunk.system_fingerprint
131
+
132
+ completion_snapshot
133
+ end
134
+
135
+ def accumulate_choice!(choice, completion_snapshot)
136
+ choice_snapshot = completion_snapshot.choices[choice.index]
137
+
138
+ if choice_snapshot.nil?
139
+ choice_snapshot = create_new_choice_snapshot(choice)
140
+ completion_snapshot.choices[choice.index] = choice_snapshot
141
+ else
142
+ update_existing_choice_snapshot(choice, choice_snapshot)
143
+ end
144
+
145
+ if choice.finish_reason
146
+ choice_snapshot.finish_reason = choice.finish_reason
147
+ handle_finish_reason(choice.finish_reason)
148
+ end
149
+
150
+ parse_tool_calls!(choice.delta.tool_calls, choice_snapshot.message.tool_calls)
151
+
152
+ accumulate_logprobs!(choice.logprobs, choice_snapshot)
153
+ end
154
+
155
+ def create_new_choice_snapshot(choice)
156
+ OpenAI::Internal::Type::Converter.coerce(
157
+ OpenAI::Models::Chat::ParsedChoice,
158
+ choice.to_h.except(:delta).merge(message: choice.delta.to_h)
159
+ )
160
+ end
161
+
162
+ def update_existing_choice_snapshot(choice, choice_snapshot)
163
+ delta_data = model_dump(choice.delta)
164
+ message_hash = model_dump(choice_snapshot.message)
165
+
166
+ accumulated_data = accumulate_delta(message_hash, delta_data)
167
+
168
+ choice_snapshot.message = OpenAI::Internal::Type::Converter.coerce(
169
+ OpenAI::Chat::ChatCompletionMessage,
170
+ accumulated_data
171
+ )
172
+ end
173
+
174
+ def build_events(chunk:, completion_snapshot:)
175
+ chunk_event = ChatChunkEvent.new(
176
+ type: :chunk,
177
+ chunk: chunk,
178
+ snapshot: completion_snapshot
179
+ )
180
+
181
+ choice_events = chunk.choices.flat_map do |choice|
182
+ build_choice_events(choice, completion_snapshot)
183
+ end
184
+
185
+ [chunk_event] + choice_events
186
+ end
187
+
188
+ def build_choice_events(choice, completion_snapshot)
189
+ choice_state = get_choice_state(choice)
190
+ choice_snapshot = completion_snapshot.choices[choice.index]
191
+
192
+ content_delta_events(choice, choice_snapshot) +
193
+ tool_call_delta_events(choice, choice_snapshot) +
194
+ logprobs_delta_events(choice, choice_snapshot) +
195
+ choice_state.get_done_events(
196
+ choice_chunk: choice,
197
+ choice_snapshot: choice_snapshot,
198
+ response_format: @response_format
199
+ )
200
+ end
201
+
202
+ def content_delta_events(choice, choice_snapshot)
203
+ events = []
204
+
205
+ if choice.delta.content && choice_snapshot.message.content
206
+ events << ChatContentDeltaEvent.new(
207
+ type: :"content.delta",
208
+ delta: choice.delta.content,
209
+ snapshot: choice_snapshot.message.content,
210
+ parsed: choice_snapshot.message.parsed
211
+ )
212
+ end
213
+
214
+ if choice.delta.refusal && choice_snapshot.message.refusal
215
+ events << ChatRefusalDeltaEvent.new(
216
+ type: :"refusal.delta",
217
+ delta: choice.delta.refusal,
218
+ snapshot: choice_snapshot.message.refusal
219
+ )
220
+ end
221
+
222
+ events
223
+ end
224
+
225
+ def tool_call_delta_events(choice, choice_snapshot)
226
+ events = []
227
+ return events unless choice.delta.tool_calls
228
+
229
+ tool_calls = choice_snapshot.message.tool_calls
230
+ return events unless tool_calls
231
+
232
+ choice.delta.tool_calls.each do |tool_call_delta|
233
+ tool_call = tool_calls[tool_call_delta.index]
234
+ next unless tool_call.type == :function && tool_call_delta.function
235
+
236
+ parsed_args = if tool_call.function.respond_to?(:parsed)
237
+ tool_call.function.parsed
238
+ end
239
+ events << ChatFunctionToolCallArgumentsDeltaEvent.new(
240
+ type: :"tool_calls.function.arguments.delta",
241
+ name: tool_call.function.name,
242
+ index: tool_call_delta.index,
243
+ arguments: tool_call.function.arguments,
244
+ parsed: parsed_args,
245
+ arguments_delta: tool_call_delta.function.arguments || ""
246
+ )
247
+ end
248
+
249
+ events
250
+ end
251
+
252
+ def logprobs_delta_events(choice, choice_snapshot)
253
+ events = []
254
+ return events unless choice.logprobs && choice_snapshot.logprobs
255
+
256
+ if choice.logprobs.content && choice_snapshot.logprobs.content
257
+ events << ChatLogprobsContentDeltaEvent.new(
258
+ type: :"logprobs.content.delta",
259
+ content: choice.logprobs.content,
260
+ snapshot: choice_snapshot.logprobs.content
261
+ )
262
+ end
263
+
264
+ if choice.logprobs.refusal && choice_snapshot.logprobs.refusal
265
+ events << ChatLogprobsRefusalDeltaEvent.new(
266
+ type: :"logprobs.refusal.delta",
267
+ refusal: choice.logprobs.refusal,
268
+ snapshot: choice_snapshot.logprobs.refusal
269
+ )
270
+ end
271
+
272
+ events
273
+ end
274
+
275
+ def handle_finish_reason(finish_reason)
276
+ return unless parseable_input?
277
+
278
+ case finish_reason
279
+ when :length
280
+ raise LengthFinishReasonError.new(completion: @chat_completion)
281
+ when :content_filter
282
+ raise ContentFilterFinishReasonError.new
283
+ end
284
+ end
285
+
286
+ def parse_tool_calls!(delta_tool_calls, snapshot_tool_calls)
287
+ return unless delta_tool_calls && snapshot_tool_calls
288
+
289
+ delta_tool_calls.each do |tool_call_chunk|
290
+ tool_call_snapshot = snapshot_tool_calls[tool_call_chunk.index]
291
+ next unless tool_call_snapshot&.type == :function
292
+
293
+ input_tool = find_input_tool(tool_call_snapshot.function.name)
294
+ next unless input_tool&.dig(:function, :strict)
295
+ next unless tool_call_snapshot.function.arguments
296
+
297
+ begin
298
+ tool_call_snapshot.function.parsed = JSON.parse(
299
+ tool_call_snapshot.function.arguments,
300
+ symbolize_names: true
301
+ )
302
+ rescue JSON::ParserError
303
+ nil
304
+ end
305
+ end
306
+ end
307
+
308
+ def accumulate_logprobs!(choice_logprobs, choice_snapshot)
309
+ return unless choice_logprobs
310
+
311
+ if choice_snapshot.logprobs.nil?
312
+ choice_snapshot.logprobs = OpenAI::Chat::ChatCompletionChunk::Choice::Logprobs.new(
313
+ content: choice_logprobs.content,
314
+ refusal: choice_logprobs.refusal
315
+ )
316
+ else
317
+ if choice_logprobs.content
318
+ choice_snapshot.logprobs.content ||= []
319
+ choice_snapshot.logprobs.content.concat(choice_logprobs.content)
320
+ end
321
+
322
+ if choice_logprobs.refusal
323
+ choice_snapshot.logprobs.refusal ||= []
324
+ choice_snapshot.logprobs.refusal.concat(choice_logprobs.refusal)
325
+ end
326
+ end
327
+ end
328
+
329
+ def parse_chat_completion(chat_completion:, response_format:)
330
+ choices = chat_completion.choices.map do |choice|
331
+ if parseable_input?
332
+ case choice.finish_reason
333
+ when :length
334
+ raise LengthFinishReasonError.new(completion: chat_completion)
335
+ when :content_filter
336
+ raise ContentFilterFinishReasonError.new
337
+ end
338
+ end
339
+
340
+ build_parsed_choice(choice, response_format)
341
+ end
342
+
343
+ OpenAI::Internal::Type::Converter.coerce(
344
+ OpenAI::Chat::ParsedChatCompletion,
345
+ chat_completion.to_h.merge(choices: choices)
346
+ )
347
+ end
348
+
349
+ def build_parsed_choice(choice, response_format)
350
+ message = choice.message
351
+
352
+ tool_calls = parse_choice_tool_calls(message.tool_calls)
353
+
354
+ choice_data = model_dump(choice)
355
+ choice_data[:message] = model_dump(message)
356
+ choice_data[:message][:tool_calls] = tool_calls && !tool_calls.empty? ? tool_calls : nil
357
+
358
+ if response_format && message.content && !message.refusal
359
+ choice_data[:message][:parsed] = parse_content(response_format, message)
360
+ end
361
+
362
+ choice_data
363
+ end
364
+
365
+ def parse_choice_tool_calls(tool_calls)
366
+ return unless tool_calls
367
+
368
+ tool_calls.map do |tool_call|
369
+ tool_call_hash = model_dump(tool_call)
370
+ next tool_call_hash unless tool_call_hash[:type] == :function && tool_call_hash[:function]
371
+
372
+ function = tool_call_hash[:function]
373
+ parsed_args = parse_function_tool_arguments(function)
374
+ function[:parsed] = parsed_args if parsed_args
375
+
376
+ tool_call_hash
377
+ end
378
+ end
379
+
380
+ def parseable_input?
381
+ @response_format || @input_tools.any?
382
+ end
383
+
384
+ def model_dump(obj)
385
+ if obj.is_a?(OpenAI::Internal::Type::BaseModel)
386
+ obj.deep_to_h
387
+ elsif obj.respond_to?(:to_h)
388
+ obj.to_h
389
+ else
390
+ obj
391
+ end
392
+ end
393
+
394
+ def find_input_tool(name)
395
+ @input_tools.find { |tool| tool.dig(:function, :name) == name }
396
+ end
397
+
398
+ def parse_function_tool_arguments(function)
399
+ return nil unless function[:arguments]
400
+
401
+ input_tool = find_input_tool(function[:name])
402
+ return nil unless input_tool&.dig(:function, :strict)
403
+
404
+ parsed = JSON.parse(function[:arguments], symbolize_names: true)
405
+ return nil unless parsed
406
+
407
+ model_class = input_tool[:model] || input_tool.dig(:function, :parameters)
408
+ if model_class.is_a?(Class)
409
+ OpenAI::Internal::Type::Converter.coerce(model_class, parsed)
410
+ else
411
+ parsed
412
+ end
413
+ rescue JSON::ParserError
414
+ nil
415
+ end
416
+
417
+ def parse_content(response_format, message)
418
+ return nil unless message.content && !message.refusal
419
+
420
+ parsed = JSON.parse(message.content, symbolize_names: true)
421
+ return nil unless parsed
422
+
423
+ if response_format.is_a?(Class)
424
+ OpenAI::Internal::Type::Converter.coerce(response_format, parsed)
425
+ else
426
+ parsed
427
+ end
428
+ rescue JSON::ParserError
429
+ nil
430
+ end
431
+
432
+ def convert_initial_chunk_into_snapshot(chunk)
433
+ data = chunk.to_h
434
+
435
+ choices = []
436
+ chunk.choices.each do |choice|
437
+ choice_hash = choice.to_h
438
+ delta_hash = choice.delta.to_h
439
+
440
+ message_data = delta_hash.dup
441
+ message_data[:role] ||= :assistant
442
+
443
+ choice_data = {
444
+ index: choice_hash[:index],
445
+ message: message_data,
446
+ finish_reason: choice_hash[:finish_reason],
447
+ logprobs: choice_hash[:logprobs]
448
+ }
449
+ choices << choice_data
450
+ end
451
+
452
+ OpenAI::Internal::Type::Converter.coerce(
453
+ OpenAI::Chat::ParsedChatCompletion,
454
+ {
455
+ id: data[:id],
456
+ object: :"chat.completion",
457
+ created: data[:created],
458
+ model: data[:model],
459
+ choices: choices,
460
+ usage: data[:usage],
461
+ system_fingerprint: nil,
462
+ service_tier: data[:service_tier]
463
+ }
464
+ )
465
+ end
466
+
467
+ def accumulate_delta(acc, delta)
468
+ return acc if delta.nil?
469
+
470
+ delta.each do |key, delta_value| # rubocop:disable Metrics/BlockLength
471
+ key = key.to_sym if key.is_a?(String)
472
+
473
+ unless acc.key?(key)
474
+ acc[key] = delta_value
475
+ next
476
+ end
477
+
478
+ acc_value = acc[key]
479
+ if acc_value.nil?
480
+ acc[key] = delta_value
481
+ next
482
+ end
483
+
484
+ # Special properties that should be replaced, not accumulated.
485
+ if [:index, :type, :parsed].include?(key)
486
+ acc[key] = delta_value
487
+ next
488
+ end
489
+
490
+ if acc_value.is_a?(String) && delta_value.is_a?(String)
491
+ acc[key] = acc_value + delta_value
492
+ elsif acc_value.is_a?(Numeric) && delta_value.is_a?(Numeric) # rubocop:disable Lint/DuplicateBranch
493
+ acc[key] = acc_value + delta_value
494
+ elsif acc_value.is_a?(Hash) && delta_value.is_a?(Hash)
495
+ acc[key] = accumulate_delta(acc_value, delta_value)
496
+ elsif acc_value.is_a?(Array) && delta_value.is_a?(Array)
497
+ if acc_value.all? { |x| x.is_a?(String) || x.is_a?(Numeric) }
498
+ acc_value.concat(delta_value)
499
+ next
500
+ end
501
+
502
+ delta_value.each do |delta_entry|
503
+ unless delta_entry.is_a?(Hash)
504
+ raise TypeError,
505
+ "Unexpected list delta entry is not a hash: #{delta_entry}"
506
+ end
507
+
508
+ index = delta_entry[:index] || delta_entry["index"]
509
+ if index.nil?
510
+ raise RuntimeError,
511
+ "Expected list delta entry to have an `index` key; #{delta_entry}"
512
+ end
513
+ unless index.is_a?(Integer)
514
+ raise TypeError,
515
+ "Unexpected, list delta entry `index` value is not an integer; #{index}"
516
+ end
517
+
518
+ if acc_value[index].nil?
519
+ acc_value[index] = delta_entry
520
+ elsif acc_value[index].is_a?(Hash)
521
+ acc_value[index] = accumulate_delta(acc_value[index], delta_entry)
522
+ end
523
+ end
524
+ else
525
+ acc[key] = acc_value
526
+ end
527
+ end
528
+
529
+ acc
530
+ end
531
+ end
532
+
533
+ class ChoiceEventState
534
+ def initialize(input_tools:)
535
+ @input_tools = Array(input_tools)
536
+ @content_done = false
537
+ @refusal_done = false
538
+ @logprobs_content_done = false
539
+ @logprobs_refusal_done = false
540
+ @done_tool_calls = Set.new
541
+ @current_tool_call_index = nil
542
+ end
543
+
544
+ def get_done_events(choice_chunk:, choice_snapshot:, response_format:)
545
+ events = []
546
+
547
+ if choice_snapshot.finish_reason
548
+ events.concat(content_done_events(choice_snapshot, response_format))
549
+
550
+ if @current_tool_call_index && !@done_tool_calls.include?(@current_tool_call_index)
551
+ event = tool_done_event(choice_snapshot, @current_tool_call_index)
552
+ events << event if event
553
+ end
554
+ end
555
+
556
+ Array(choice_chunk.delta.tool_calls).each do |tool_call|
557
+ if @current_tool_call_index != tool_call.index
558
+ events.concat(content_done_events(choice_snapshot, response_format))
559
+
560
+ if @current_tool_call_index
561
+ event = tool_done_event(choice_snapshot, @current_tool_call_index)
562
+ events << event if event
563
+ end
564
+ end
565
+
566
+ @current_tool_call_index = tool_call.index
567
+ end
568
+
569
+ events
570
+ end
571
+
572
+ private
573
+
574
+ def content_done_events(choice_snapshot, response_format)
575
+ events = []
576
+
577
+ if choice_snapshot.message.content && !@content_done
578
+ @content_done = true
579
+ parsed = parse_content(choice_snapshot.message, response_format)
580
+ choice_snapshot.message.parsed = parsed
581
+
582
+ events << ChatContentDoneEvent.new(
583
+ type: :"content.done",
584
+ content: choice_snapshot.message.content,
585
+ parsed: parsed
586
+ )
587
+ end
588
+
589
+ if choice_snapshot.message.refusal && !@refusal_done
590
+ @refusal_done = true
591
+ events << ChatRefusalDoneEvent.new(
592
+ type: :"refusal.done",
593
+ refusal: choice_snapshot.message.refusal
594
+ )
595
+ end
596
+
597
+ events + logprobs_done_events(choice_snapshot)
598
+ end
599
+
600
+ def logprobs_done_events(choice_snapshot)
601
+ events = []
602
+ logprobs = choice_snapshot.logprobs
603
+ return events unless logprobs
604
+
605
+ if logprobs.content&.any? && !@logprobs_content_done
606
+ @logprobs_content_done = true
607
+ events << ChatLogprobsContentDoneEvent.new(
608
+ type: :"logprobs.content.done",
609
+ content: logprobs.content
610
+ )
611
+ end
612
+
613
+ if logprobs.refusal&.any? && !@logprobs_refusal_done
614
+ @logprobs_refusal_done = true
615
+ events << ChatLogprobsRefusalDoneEvent.new(
616
+ type: :"logprobs.refusal.done",
617
+ refusal: logprobs.refusal
618
+ )
619
+ end
620
+
621
+ events
622
+ end
623
+
624
+ def tool_done_event(choice_snapshot, tool_index)
625
+ return nil if @done_tool_calls.include?(tool_index)
626
+
627
+ @done_tool_calls.add(tool_index)
628
+
629
+ tool_call = choice_snapshot.message.tool_calls&.[](tool_index)
630
+ return nil unless tool_call&.type == :function
631
+
632
+ parsed_args = parse_function_tool_arguments(tool_call.function)
633
+
634
+ if tool_call.function.respond_to?(:parsed=)
635
+ tool_call.function.parsed = parsed_args
636
+ end
637
+
638
+ ChatFunctionToolCallArgumentsDoneEvent.new(
639
+ type: :"tool_calls.function.arguments.done",
640
+ index: tool_index,
641
+ name: tool_call.function.name,
642
+ arguments: tool_call.function.arguments,
643
+ parsed: parsed_args
644
+ )
645
+ end
646
+
647
+ def parse_content(message, response_format)
648
+ return nil unless response_format && message.content
649
+
650
+ parsed = JSON.parse(message.content, symbolize_names: true)
651
+ if response_format.is_a?(Class)
652
+ OpenAI::Internal::Type::Converter.coerce(response_format, parsed)
653
+ else
654
+ parsed
655
+ end
656
+ rescue JSON::ParserError
657
+ nil
658
+ end
659
+
660
+ def parse_function_tool_arguments(function)
661
+ return nil unless function.arguments
662
+
663
+ tool = find_input_tool(function.name)
664
+ return nil unless tool&.dig(:function, :strict)
665
+
666
+ parsed = JSON.parse(function.arguments, symbolize_names: true)
667
+
668
+ if tool[:model]
669
+ OpenAI::Internal::Type::Converter.coerce(tool[:model], parsed)
670
+ else
671
+ parsed
672
+ end
673
+ rescue JSON::ParserError
674
+ nil
675
+ end
676
+
677
+ def find_input_tool(name)
678
+ @input_tools.find { |tool| tool.dig(:function, :name) == name }
679
+ end
680
+ end
681
+ end
682
+ end
683
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenAI
4
+ module Helpers
5
+ module Streaming
6
+ # Raw streaming chunk event with accumulated completion snapshot.
7
+ #
8
+ # This is the fundamental event that wraps each raw chunk from the API
9
+ # along with the accumulated state up to that point. All other events
10
+ # are derived from processing these chunks.
11
+ #
12
+ # @example
13
+ # event.chunk # => ChatCompletionChunk (raw API response)
14
+ # event.snapshot # => ParsedChatCompletion (accumulated state)
15
+ class ChatChunkEvent < OpenAI::Internal::Type::BaseModel
16
+ required :type, const: :chunk
17
+ required :chunk, -> { OpenAI::Chat::ChatCompletionChunk }
18
+ required :snapshot, -> { OpenAI::Chat::ParsedChatCompletion }
19
+ end
20
+
21
+ # Incremental text content update event.
22
+ #
23
+ # Emitted as the assistant's text response is being generated. Each event
24
+ # contains the new text fragment (delta) and the complete accumulated
25
+ # text so far (snapshot).
26
+ #
27
+ # @example
28
+ # event.delta # => "Hello" (new fragment)
29
+ # event.snapshot # => "Hello world" (accumulated text)
30
+ # event.parsed # => {name: "John"} (if using structured outputs)
31
+ class ChatContentDeltaEvent < OpenAI::Internal::Type::BaseModel
32
+ required :type, const: :"content.delta"
33
+ required :delta, String
34
+ required :snapshot, String
35
+ optional :parsed, Object # Partially parsed structured output
36
+ end
37
+
38
+ # Text content completion event.
39
+ #
40
+ # Emitted when the assistant has finished generating text content.
41
+ # Contains the complete text and, if applicable, the fully parsed
42
+ # structured output.
43
+ #
44
+ # @example
45
+ # event.content # => "Hello world! How can I help?"
46
+ # event.parsed # => {name: "John", age: 30} (if using structured outputs)
47
+ class ChatContentDoneEvent < OpenAI::Internal::Type::BaseModel
48
+ required :type, const: :"content.done"
49
+ required :content, String
50
+ optional :parsed, Object # Fully parsed structured output
51
+ end
52
+
53
+ # Incremental refusal update event.
54
+ #
55
+ # Emitted when the assistant is refusing to fulfill a request.
56
+ # Contains the new refusal text fragment and accumulated refusal message.
57
+ #
58
+ # @example
59
+ # event.delta # => "I cannot"
60
+ # event.snapshot # => "I cannot help with that request"
61
+ class ChatRefusalDeltaEvent < OpenAI::Internal::Type::BaseModel
62
+ required :type, const: :"refusal.delta"
63
+ required :delta, String
64
+ required :snapshot, String
65
+ end
66
+
67
+ # Refusal completion event.
68
+ #
69
+ # Emitted when the assistant has finished generating a refusal message.
70
+ # Contains the complete refusal text.
71
+ #
72
+ # @example
73
+ # event.refusal # => "I cannot help with that request as it violates..."
74
+ class ChatRefusalDoneEvent < OpenAI::Internal::Type::BaseModel
75
+ required :type, const: :"refusal.done"
76
+ required :refusal, String
77
+ end
78
+
79
+ # Incremental function tool call arguments update.
80
+ #
81
+ # Emitted as function arguments are being streamed. Provides both the
82
+ # raw JSON fragments and incrementally parsed arguments for strict tools.
83
+ #
84
+ # @example
85
+ # event.name # => "get_weather"
86
+ # event.index # => 0 (tool call index in array)
87
+ # event.arguments_delta # => '{"location": "San' (new fragment)
88
+ # event.arguments # => '{"location": "San Francisco"' (accumulated JSON)
89
+ # event.parsed # => {location: "San Francisco"} (if strict: true)
90
+ class ChatFunctionToolCallArgumentsDeltaEvent < OpenAI::Internal::Type::BaseModel
91
+ required :type, const: :"tool_calls.function.arguments.delta"
92
+ required :name, String
93
+ required :index, Integer
94
+ required :arguments_delta, String
95
+ required :arguments, String
96
+ required :parsed, Object
97
+ end
98
+
99
+ # Function tool call arguments completion event.
100
+ #
101
+ # Emitted when a function tool call's arguments are complete.
102
+ # For tools defined with `strict: true`, the arguments will be fully
103
+ # parsed and validated. For non-strict tools, only raw JSON is available.
104
+ #
105
+ # @example With strict tool
106
+ # event.name # => "get_weather"
107
+ # event.arguments # => '{"location": "San Francisco", "unit": "celsius"}'
108
+ # event.parsed # => {location: "San Francisco", unit: "celsius"}
109
+ #
110
+ # @example Without strict tool
111
+ # event.parsed # => nil (parse JSON from event.arguments manually)
112
+ class ChatFunctionToolCallArgumentsDoneEvent < OpenAI::Internal::Type::BaseModel
113
+ required :type, const: :"tool_calls.function.arguments.done"
114
+ required :name, String
115
+ required :index, Integer
116
+ required :arguments, String
117
+ required :parsed, Object # (only for strict: true tools)
118
+ end
119
+
120
+ # Incremental logprobs update for content tokens.
121
+ #
122
+ # Emitted when logprobs are requested and content tokens are being generated.
123
+ # Contains log probability information for the new tokens and accumulated
124
+ # logprobs for all content tokens so far.
125
+ #
126
+ # @example
127
+ # event.content[0].token # => "Hello"
128
+ # event.content[0].logprob # => -0.31725305
129
+ # event.content[0].top_logprobs # => [{token: "Hello", logprob: -0.31725305}, ...]
130
+ # event.snapshot # => [all logprobs accumulated so far]
131
+ class ChatLogprobsContentDeltaEvent < OpenAI::Internal::Type::BaseModel
132
+ required :type, const: :"logprobs.content.delta"
133
+ required :content, -> { OpenAI::Internal::Type::ArrayOf[OpenAI::Chat::ChatCompletionTokenLogprob] }
134
+ required :snapshot, -> { OpenAI::Internal::Type::ArrayOf[OpenAI::Chat::ChatCompletionTokenLogprob] }
135
+ end
136
+
137
+ # Logprobs completion event for content tokens.
138
+ #
139
+ # Emitted when content generation is complete and logprobs were requested.
140
+ # Contains the complete array of log probabilities for all content tokens.
141
+ #
142
+ # @example
143
+ # event.content.each do |logprob|
144
+ # puts "Token: #{logprob.token}, Logprob: #{logprob.logprob}"
145
+ # end
146
+ class ChatLogprobsContentDoneEvent < OpenAI::Internal::Type::BaseModel
147
+ required :type, const: :"logprobs.content.done"
148
+ required :content, -> { OpenAI::Internal::Type::ArrayOf[OpenAI::Chat::ChatCompletionTokenLogprob] }
149
+ end
150
+
151
+ # Incremental logprobs update for refusal tokens.
152
+ #
153
+ # Emitted when logprobs are requested and refusal tokens are being generated.
154
+ # Contains log probability information for refusal message tokens.
155
+ #
156
+ # @example
157
+ # event.refusal[0].token # => "I"
158
+ # event.refusal[0].logprob # => -0.12345
159
+ # event.snapshot # => [all refusal logprobs accumulated so far]
160
+ class ChatLogprobsRefusalDeltaEvent < OpenAI::Internal::Type::BaseModel
161
+ required :type, const: :"logprobs.refusal.delta"
162
+ required :refusal, -> { OpenAI::Internal::Type::ArrayOf[OpenAI::Chat::ChatCompletionTokenLogprob] }
163
+ required :snapshot, -> { OpenAI::Internal::Type::ArrayOf[OpenAI::Chat::ChatCompletionTokenLogprob] }
164
+ end
165
+
166
+ # Logprobs completion event for refusal tokens.
167
+ #
168
+ # Emitted when refusal generation is complete and logprobs were requested.
169
+ # Contains the complete array of log probabilities for all refusal tokens.
170
+ #
171
+ # @example
172
+ # event.refusal.each do |logprob|
173
+ # puts "Refusal token: #{logprob.token}, Logprob: #{logprob.logprob}"
174
+ # end
175
+ class ChatLogprobsRefusalDoneEvent < OpenAI::Internal::Type::BaseModel
176
+ required :type, const: :"logprobs.refusal.done"
177
+ required :refusal, -> { OpenAI::Internal::Type::ArrayOf[OpenAI::Chat::ChatCompletionTokenLogprob] }
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenAI
4
+ module Helpers
5
+ module Streaming
6
+ class StreamError < StandardError; end
7
+
8
+ class LengthFinishReasonError < StreamError
9
+ attr_reader :completion
10
+
11
+ def initialize(completion:)
12
+ @completion = completion
13
+ super("Stream finished due to length limit")
14
+ end
15
+ end
16
+
17
+ class ContentFilterFinishReasonError < StreamError
18
+ def initialize
19
+ super("Stream finished due to content filter")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ module OpenAI
27
+ LengthFinishReasonError = Helpers::Streaming::LengthFinishReasonError
28
+ ContentFilterFinishReasonError = Helpers::Streaming::ContentFilterFinishReasonError
29
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "events"
4
-
5
3
  module OpenAI
6
4
  module Helpers
7
5
  module Streaming
@@ -566,7 +566,8 @@ module OpenAI
566
566
  #
567
567
  # @return [Array(String, Enumerable<String>)]
568
568
  private def encode_multipart_streaming(body)
569
- boundary = SecureRandom.urlsafe_base64(60)
569
+ # RFC 1521 Section 7.2.1 says we should have 70 char maximum for boundary length
570
+ boundary = SecureRandom.urlsafe_base64(46)
570
571
 
571
572
  closing = []
572
573
  strio = writable_enum do |y|
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenAI
4
+ module Models
5
+ module Chat
6
+ class ParsedChoice < OpenAI::Models::Chat::ChatCompletion::Choice
7
+ optional :finish_reason, enum: -> { OpenAI::Chat::ChatCompletion::Choice::FinishReason }, nil?: true
8
+ end
9
+
10
+ class ParsedChatCompletion < ChatCompletion
11
+ required :choices, -> { OpenAI::Internal::Type::ArrayOf[ParsedChoice] }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -110,6 +110,54 @@ module OpenAI
110
110
  raise ArgumentError.new(message)
111
111
  end
112
112
 
113
+ model, tool_models = get_structured_output_models(parsed)
114
+
115
+ # rubocop:disable Metrics/BlockLength
116
+ unwrap = ->(raw) do
117
+ if model.is_a?(OpenAI::StructuredOutput::JsonSchemaConverter)
118
+ raw[:choices]&.each do |choice|
119
+ message = choice.fetch(:message)
120
+ begin
121
+ content = message.fetch(:content)
122
+ parsed = content.nil? ? nil : JSON.parse(content, symbolize_names: true)
123
+ rescue JSON::ParserError => e
124
+ parsed = e
125
+ end
126
+ coerced = OpenAI::Internal::Type::Converter.coerce(model, parsed)
127
+ message.store(:parsed, coerced)
128
+ end
129
+ end
130
+ raw[:choices]&.each do |choice|
131
+ choice.dig(:message, :tool_calls)&.each do |tool_call|
132
+ func = tool_call.fetch(:function)
133
+ next if (model = tool_models[func.fetch(:name)]).nil?
134
+
135
+ begin
136
+ arguments = func.fetch(:arguments)
137
+ parsed = arguments.nil? ? nil : JSON.parse(arguments, symbolize_names: true)
138
+ rescue JSON::ParserError => e
139
+ parsed = e
140
+ end
141
+ coerced = OpenAI::Internal::Type::Converter.coerce(model, parsed)
142
+ func.store(:parsed, coerced)
143
+ end
144
+ end
145
+
146
+ raw
147
+ end
148
+ # rubocop:enable Metrics/BlockLength
149
+
150
+ @client.request(
151
+ method: :post,
152
+ path: "chat/completions",
153
+ body: parsed,
154
+ unwrap: unwrap,
155
+ model: OpenAI::Chat::ChatCompletion,
156
+ options: options
157
+ )
158
+ end
159
+
160
+ def get_structured_output_models(parsed)
113
161
  model = nil
114
162
  tool_models = {}
115
163
  case parsed
@@ -162,53 +210,46 @@ module OpenAI
162
210
  else
163
211
  end
164
212
 
165
- # rubocop:disable Metrics/BlockLength
166
- unwrap = ->(raw) do
167
- if model.is_a?(OpenAI::StructuredOutput::JsonSchemaConverter)
168
- raw[:choices]&.each do |choice|
169
- message = choice.fetch(:message)
170
- begin
171
- content = message.fetch(:content)
172
- parsed = content.nil? ? nil : JSON.parse(content, symbolize_names: true)
173
- rescue JSON::ParserError => e
174
- parsed = e
175
- end
176
- coerced = OpenAI::Internal::Type::Converter.coerce(model, parsed)
177
- message.store(:parsed, coerced)
178
- end
179
- end
180
- raw[:choices]&.each do |choice|
181
- choice.dig(:message, :tool_calls)&.each do |tool_call|
182
- func = tool_call.fetch(:function)
183
- next if (model = tool_models[func.fetch(:name)]).nil?
213
+ [model, tool_models]
214
+ end
184
215
 
185
- begin
186
- arguments = func.fetch(:arguments)
187
- parsed = arguments.nil? ? nil : JSON.parse(arguments, symbolize_names: true)
188
- rescue JSON::ParserError => e
189
- parsed = e
190
- end
191
- coerced = OpenAI::Internal::Type::Converter.coerce(model, parsed)
192
- func.store(:parsed, coerced)
193
- end
194
- end
216
+ def build_tools_with_models(tools, tool_models)
217
+ return [] if tools.nil?
195
218
 
196
- raw
219
+ tools.map do |tool|
220
+ next tool unless tool[:type] == :function
221
+
222
+ function_name = tool.dig(:function, :name)
223
+ model = tool_models[function_name]
224
+
225
+ model ? tool.merge(model: model) : tool
197
226
  end
198
- # rubocop:enable Metrics/BlockLength
227
+ end
199
228
 
200
- @client.request(
229
+ def stream(params)
230
+ parsed, options = OpenAI::Chat::CompletionCreateParams.dump_request(params)
231
+
232
+ parsed.store(:stream, true)
233
+
234
+ response_format, tool_models = get_structured_output_models(parsed)
235
+
236
+ input_tools = build_tools_with_models(parsed[:tools], tool_models)
237
+
238
+ raw_stream = @client.request(
201
239
  method: :post,
202
240
  path: "chat/completions",
241
+ headers: {"accept" => "text/event-stream"},
203
242
  body: parsed,
204
- unwrap: unwrap,
205
- model: OpenAI::Chat::ChatCompletion,
243
+ stream: OpenAI::Internal::Stream,
244
+ model: OpenAI::Chat::ChatCompletionChunk,
206
245
  options: options
207
246
  )
208
- end
209
247
 
210
- def stream
211
- raise NotImplementedError.new("higher level helpers are coming soon!")
248
+ OpenAI::Helpers::Streaming::ChatCompletionStream.new(
249
+ raw_stream: raw_stream,
250
+ response_format: response_format,
251
+ input_tools: input_tools
252
+ )
212
253
  end
213
254
 
214
255
  # See {OpenAI::Resources::Chat::Completions#create} for non-streaming counterpart.
@@ -85,7 +85,7 @@ module OpenAI
85
85
  def create(params = {})
86
86
  parsed, options = OpenAI::Responses::ResponseCreateParams.dump_request(params)
87
87
  if parsed[:stream]
88
- message = "Please use `#stream_raw` for the streaming use case."
88
+ message = "Please use `#stream` for the streaming use case."
89
89
  raise ArgumentError.new(message)
90
90
  end
91
91
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenAI
4
- VERSION = "0.26.0"
4
+ VERSION = "0.27.0"
5
5
  end
data/lib/openai.rb CHANGED
@@ -195,6 +195,7 @@ require_relative "openai/models/chat/chat_completion_assistant_message_param"
195
195
  require_relative "openai/models/chat/chat_completion_audio"
196
196
  require_relative "openai/models/chat/chat_completion_audio_param"
197
197
  require_relative "openai/models/chat/chat_completion_chunk"
198
+ require_relative "openai/models/chat/parsed_chat_completion"
198
199
  require_relative "openai/models/chat/chat_completion_content_part"
199
200
  require_relative "openai/models/chat/chat_completion_content_part_image"
200
201
  require_relative "openai/models/chat/chat_completion_content_part_input_audio"
@@ -697,6 +698,9 @@ require_relative "openai/resources/vector_stores"
697
698
  require_relative "openai/resources/vector_stores/file_batches"
698
699
  require_relative "openai/resources/vector_stores/files"
699
700
  require_relative "openai/resources/webhooks"
700
- require_relative "openai/helpers/streaming/events"
701
+ require_relative "openai/helpers/streaming/response_events"
701
702
  require_relative "openai/helpers/streaming/response_stream"
703
+ require_relative "openai/helpers/streaming/exceptions"
704
+ require_relative "openai/helpers/streaming/chat_events"
705
+ require_relative "openai/helpers/streaming/chat_completion_stream"
702
706
  require_relative "openai/streaming"
@@ -26,6 +26,126 @@ module OpenAI
26
26
  def response
27
27
  end
28
28
  end
29
+
30
+ class ChatChunkEvent < OpenAI::Internal::Type::BaseModel
31
+ sig { returns(T.untyped) }
32
+ def chunk
33
+ end
34
+
35
+ sig { returns(T.untyped) }
36
+ def snapshot
37
+ end
38
+ end
39
+
40
+ class ChatContentDeltaEvent < OpenAI::Internal::Type::BaseModel
41
+ sig { returns(String) }
42
+ def delta
43
+ end
44
+
45
+ sig { returns(String) }
46
+ def snapshot
47
+ end
48
+
49
+ sig { returns(T.untyped) }
50
+ def parsed
51
+ end
52
+ end
53
+
54
+ class ChatContentDoneEvent < OpenAI::Internal::Type::BaseModel
55
+ sig { returns(String) }
56
+ def content
57
+ end
58
+
59
+ sig { returns(T.untyped) }
60
+ def parsed
61
+ end
62
+ end
63
+
64
+ class ChatRefusalDeltaEvent < OpenAI::Internal::Type::BaseModel
65
+ sig { returns(String) }
66
+ def delta
67
+ end
68
+
69
+ sig { returns(String) }
70
+ def snapshot
71
+ end
72
+ end
73
+
74
+ class ChatRefusalDoneEvent < OpenAI::Internal::Type::BaseModel
75
+ sig { returns(String) }
76
+ def refusal
77
+ end
78
+ end
79
+
80
+ class ChatFunctionToolCallArgumentsDeltaEvent < OpenAI::Internal::Type::BaseModel
81
+ sig { returns(String) }
82
+ def name
83
+ end
84
+
85
+ sig { returns(Integer) }
86
+ def index
87
+ end
88
+
89
+ sig { returns(String) }
90
+ def arguments_delta
91
+ end
92
+
93
+ sig { returns(String) }
94
+ def arguments
95
+ end
96
+
97
+ sig { returns(T.untyped) }
98
+ def parsed_arguments
99
+ end
100
+ end
101
+
102
+ class ChatFunctionToolCallArgumentsDoneEvent < OpenAI::Internal::Type::BaseModel
103
+ sig { returns(String) }
104
+ def name
105
+ end
106
+
107
+ sig { returns(Integer) }
108
+ def index
109
+ end
110
+
111
+ sig { returns(String) }
112
+ def arguments
113
+ end
114
+
115
+ sig { returns(T.untyped) }
116
+ def parsed_arguments
117
+ end
118
+ end
119
+
120
+ class ChatLogprobsContentDeltaEvent < OpenAI::Internal::Type::BaseModel
121
+ sig { returns(T.untyped) }
122
+ def content
123
+ end
124
+ end
125
+
126
+ class ChatLogprobsContentDoneEvent < OpenAI::Internal::Type::BaseModel
127
+ sig { returns(T.untyped) }
128
+ def content
129
+ end
130
+ end
131
+
132
+ class ChatLogprobsRefusalDeltaEvent < OpenAI::Internal::Type::BaseModel
133
+ sig { returns(T.untyped) }
134
+ def refusal
135
+ end
136
+ end
137
+
138
+ class ChatLogprobsRefusalDoneEvent < OpenAI::Internal::Type::BaseModel
139
+ sig { returns(T.untyped) }
140
+ def refusal
141
+ end
142
+ end
143
+
144
+ class ChatCompletionStream
145
+ sig { returns(T.untyped) }
146
+ def each
147
+ end
148
+ end
29
149
  end
30
150
  end
31
151
  end
@@ -1,5 +1,32 @@
1
1
  # typed: strong
2
2
 
3
3
  module OpenAI
4
- Streaming = OpenAI::Helpers::Streaming
4
+ module Streaming
5
+ ResponseTextDeltaEvent = OpenAI::Helpers::Streaming::ResponseTextDeltaEvent
6
+ ResponseTextDoneEvent = OpenAI::Helpers::Streaming::ResponseTextDoneEvent
7
+ ResponseFunctionCallArgumentsDeltaEvent =
8
+ OpenAI::Helpers::Streaming::ResponseFunctionCallArgumentsDeltaEvent
9
+ ResponseCompletedEvent = OpenAI::Helpers::Streaming::ResponseCompletedEvent
10
+
11
+ ChatChunkEvent = OpenAI::Helpers::Streaming::ChatChunkEvent
12
+ ChatContentDeltaEvent = OpenAI::Helpers::Streaming::ChatContentDeltaEvent
13
+ ChatContentDoneEvent = OpenAI::Helpers::Streaming::ChatContentDoneEvent
14
+ ChatRefusalDeltaEvent = OpenAI::Helpers::Streaming::ChatRefusalDeltaEvent
15
+ ChatRefusalDoneEvent = OpenAI::Helpers::Streaming::ChatRefusalDoneEvent
16
+ ChatFunctionToolCallArgumentsDeltaEvent =
17
+ OpenAI::Helpers::Streaming::ChatFunctionToolCallArgumentsDeltaEvent
18
+ ChatFunctionToolCallArgumentsDoneEvent =
19
+ OpenAI::Helpers::Streaming::ChatFunctionToolCallArgumentsDoneEvent
20
+ ChatLogprobsContentDeltaEvent =
21
+ OpenAI::Helpers::Streaming::ChatLogprobsContentDeltaEvent
22
+ ChatLogprobsContentDoneEvent =
23
+ OpenAI::Helpers::Streaming::ChatLogprobsContentDoneEvent
24
+ ChatLogprobsRefusalDeltaEvent =
25
+ OpenAI::Helpers::Streaming::ChatLogprobsRefusalDeltaEvent
26
+ ChatLogprobsRefusalDoneEvent =
27
+ OpenAI::Helpers::Streaming::ChatLogprobsRefusalDoneEvent
28
+
29
+ ResponseStream = OpenAI::Helpers::Streaming::ResponseStream
30
+ ChatCompletionStream = OpenAI::Helpers::Streaming::ChatCompletionStream
31
+ end
5
32
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.26.0
4
+ version: 0.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenAI
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-09-23 00:00:00.000000000 Z
11
+ date: 2025-09-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: connection_pool
@@ -39,7 +39,10 @@ files:
39
39
  - lib/openai/client.rb
40
40
  - lib/openai/errors.rb
41
41
  - lib/openai/file_part.rb
42
- - lib/openai/helpers/streaming/events.rb
42
+ - lib/openai/helpers/streaming/chat_completion_stream.rb
43
+ - lib/openai/helpers/streaming/chat_events.rb
44
+ - lib/openai/helpers/streaming/exceptions.rb
45
+ - lib/openai/helpers/streaming/response_events.rb
43
46
  - lib/openai/helpers/streaming/response_stream.rb
44
47
  - lib/openai/helpers/structured_output.rb
45
48
  - lib/openai/helpers/structured_output/array_of.rb
@@ -229,6 +232,7 @@ files:
229
232
  - lib/openai/models/chat/completion_retrieve_params.rb
230
233
  - lib/openai/models/chat/completion_update_params.rb
231
234
  - lib/openai/models/chat/completions/message_list_params.rb
235
+ - lib/openai/models/chat/parsed_chat_completion.rb
232
236
  - lib/openai/models/chat_model.rb
233
237
  - lib/openai/models/comparison_filter.rb
234
238
  - lib/openai/models/completion.rb