Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .castiron.stats.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
schema_version: 1
generation_id: cf94c82f-0604-4dfc-aff0-cec482e84953
openapi_spec_hash: ce912f79d7f8cb19a7a515513c214b8a
openapi_transformed_spec_hash: 352b5a7a028f990b6475b6593e381eee
config_hash: dde98d9c41cd6f4163320f92c70f83e6
codegen_sha: 326831af0c0fdf859335a189df2b6716e4b605d4
codegen_hash: 00e7f6a53e1a6a1adf02385bc368fce999fdd775d1f1c28615f790c7a4109ad0
public_codegen_sha: c2abe7c282c83c8fb9a7330254235c20b67a2b10
generation_id: 1adb01cf-0ccd-40c4-9b52-01a4c8a93334
openapi_spec_hash: f6e87553c8927460254053bc57e15965
openapi_transformed_spec_hash: 6e67b20b0fdae6b9604eddb74b8ed579
config_hash: 9e898f4e0d2bc336a854079cff546681
codegen_sha: 04de33a1417a85263b0056f306860d548229620b
codegen_hash: b10163fadd6166d075d7a4a8b2b340da85dab58cfda9325f03146d618d53e2a6
public_codegen_sha: d955f54bcb210631fb4a210427ecd96aa764e2c7
4 changes: 4 additions & 0 deletions api_reference/openapi.transformed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53927,6 +53927,8 @@ components:
const: session.created
session:
description: The session configuration.
discriminator:
propertyName: type
anyOf:
- $ref: '#/components/schemas/RealtimeSessionCreateRequestGA'
- $ref: '#/components/schemas/RealtimeTranscriptionSessionCreateRequestGA'
Expand Down Expand Up @@ -54001,6 +54003,8 @@ components:
const: session.updated
session:
description: The session configuration.
discriminator:
propertyName: type
anyOf:
- $ref: '#/components/schemas/RealtimeSessionCreateRequestGA'
- $ref: '#/components/schemas/RealtimeTranscriptionSessionCreateRequestGA'
Expand Down
18 changes: 18 additions & 0 deletions lib/openai/helpers/realtime/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ def parse_event(data)
return OpenAI::Realtime::UnknownServerEvent.new(data: parsed)
end

session_union = case type.to_s
when "session.created"
OpenAI::Realtime::SessionCreatedEvent::Session
when "session.updated"
OpenAI::Realtime::SessionUpdatedEvent::Session
end

if session_union
session = parsed[:session]
unless session.is_a?(Hash) && session[:type].is_a?(String)
raise ArgumentError, "Realtime session must be an object with a string type"
end

unless discriminator_values(session_union).key?(session[:type])
raise ArgumentError, "Unsupported Realtime session type"
end
end

state = OpenAI::Internal::Type::Converter.new_coerce_state
event = OpenAI::Internal::Type::Converter.coerce(
OpenAI::Realtime::RealtimeServerEvent,
Expand Down
6 changes: 4 additions & 2 deletions lib/openai/models/realtime/session_created_event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ class SessionCreatedEvent < OpenAI::Internal::Type::BaseModel
module Session
extend OpenAI::Internal::Type::Union

discriminator :type

# Realtime session object configuration.
variant -> { OpenAI::Realtime::RealtimeSessionCreateRequest }
variant :realtime, -> { OpenAI::Realtime::RealtimeSessionCreateRequest }

# Realtime transcription session object configuration.
variant -> { OpenAI::Realtime::RealtimeTranscriptionSessionCreateRequest }
variant :transcription, -> { OpenAI::Realtime::RealtimeTranscriptionSessionCreateRequest }

# @!method self.variants
# @return [Array(OpenAI::Models::Realtime::RealtimeSessionCreateRequest, OpenAI::Models::Realtime::RealtimeTranscriptionSessionCreateRequest)]
Expand Down
6 changes: 4 additions & 2 deletions lib/openai/models/realtime/session_updated_event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ class SessionUpdatedEvent < OpenAI::Internal::Type::BaseModel
module Session
extend OpenAI::Internal::Type::Union

discriminator :type

# Realtime session object configuration.
variant -> { OpenAI::Realtime::RealtimeSessionCreateRequest }
variant :realtime, -> { OpenAI::Realtime::RealtimeSessionCreateRequest }

# Realtime transcription session object configuration.
variant -> { OpenAI::Realtime::RealtimeTranscriptionSessionCreateRequest }
variant :transcription, -> { OpenAI::Realtime::RealtimeTranscriptionSessionCreateRequest }

# @!method self.variants
# @return [Array(OpenAI::Models::Realtime::RealtimeSessionCreateRequest, OpenAI::Models::Realtime::RealtimeTranscriptionSessionCreateRequest)]
Expand Down
106 changes: 106 additions & 0 deletions test/openai/realtime/session_event_validation_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# frozen_string_literal: true

require_relative "connection_test_support"

class OpenAI::Test::RealtimeSessionEventValidationTest < Minitest::Test
include OpenAI::Test::RealtimeConnectionTestSupport

def test_supported_session_events_remain_typed
event_classes.each do |type, event_class|
event = receive({type: type, session: {type: "realtime", model: "gpt-realtime"}})

assert_instance_of(event_class, event)
assert_instance_of(OpenAI::Realtime::RealtimeSessionCreateRequest, event.session)
assert_equal(:realtime, event.session.type)

event = receive({type: type, session: transcription_session}, transcription: true)

assert_instance_of(event_class, event)
assert_instance_of(OpenAI::Realtime::RealtimeTranscriptionSessionCreateRequest, event.session)
assert_equal(:transcription, event.session.type)
assert_equal("sess_synthetic", event.session.to_h.fetch(:id))
refute_includes(event.session.to_h.keys, :client_secret)
assert_nil(event.session.include)
assert_nil(event.session.audio.input.transcription)
assert_nil(event.session.audio.input.noise_reduction)
assert_nil(event.session.audio.input.turn_detection)
end
end

def test_known_session_events_reject_missing_or_invalid_session_shapes
invalid_sessions = [nil, [], "transcription", 7, true, {}]
invalid_sessions.concat(
[nil, 7, true, {}, [], "unsupported-private-session"].map { {type: _1} }
)

event_classes.each_key do |type|
assert_protocol_error(type: type)
invalid_sessions.each { |session| assert_protocol_error(type: type, session: session) }
end
end

def test_supported_transcription_tag_still_rejects_malformed_fields
event_classes.each_key do |type|
assert_protocol_error(type: type, session: {type: "transcription", include: 7})
end
end

def test_unknown_outer_event_names_remain_observable
%w[future.unknown.event transcription_session.created transcription_session.updated].each do |type|
payload = {type: type, event_id: "evt_synthetic", session: {type: "unsupported-private-session"}}
event = receive(payload)

assert_instance_of(OpenAI::Realtime::UnknownServerEvent, event)
assert_equal(type.to_sym, event.type)
assert_equal(payload, event.to_h)
assert_predicate(event, :frozen?)
assert_predicate(event.data.fetch(:session), :frozen?)
end
end

private def event_classes
{
"session.created" => OpenAI::Realtime::SessionCreatedEvent,
"session.updated" => OpenAI::Realtime::SessionUpdatedEvent
}
end

private def transcription_session
{
type: "transcription",
object: "realtime.transcription_session",
id: "sess_synthetic",
expires_at: 2_000_000_000,
include: nil,
audio: {
input: {
format: {type: "audio/pcm", rate: 24_000},
transcription: nil,
noise_reduction: nil,
turn_detection: nil
}
}
}
end

private def receive(payload, transcription: false)
socket = FakeSocket.new(JSON.generate({event_id: "evt_synthetic", **payload}))
transport = FakeTransport.new(socket)
if transcription
client.realtime.connect_transcription(transport: transport, &:receive)
else
client.realtime.connect(model: "gpt-realtime", transport: transport, &:receive)
end

ensure
assert_predicate(socket, :closed?)
end

private def assert_protocol_error(payload)
error = assert_raises(OpenAI::Errors::RealtimeProtocolError) { receive(payload) }
assert_equal("Invalid Realtime WebSocket event.", error.message)
refute_includes(error.message, "unsupported-private-session")
refute_includes(error.full_message, "unsupported-private-session")
refute_includes(error.cause.message, "unsupported-private-session")
end
end
Loading