diff --git a/Gemfile b/Gemfile index 85894780..355fcc64 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,8 @@ gemspec gem "byebug", "~> 11.1.3" gem "dotenv", "~> 2.8.1" +gem "eventmachine", "~> 1.2.7" +gem "faye-websocket", "~> 0.11.3" gem "rake", "~> 13.2" gem "rspec", "~> 3.13" gem "rubocop", "~> 1.50.2" diff --git a/Gemfile.lock b/Gemfile.lock index 56f04af6..d81c2f05 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -21,6 +21,7 @@ GEM diff-lcs (1.5.1) dotenv (2.8.1) event_stream_parser (1.0.0) + eventmachine (1.2.7) faraday (2.8.1) base64 faraday-net_http (>= 2.0, < 3.1) @@ -28,6 +29,9 @@ GEM faraday-multipart (1.0.4) multipart-post (~> 2) faraday-net_http (3.0.2) + faye-websocket (0.11.3) + eventmachine (>= 0.12.0) + websocket-driver (>= 0.5.1) hashdiff (1.1.0) json (2.6.3) multipart-post (2.3.0) @@ -74,6 +78,9 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) + websocket-driver (0.7.6) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) PLATFORMS ruby @@ -81,6 +88,8 @@ PLATFORMS DEPENDENCIES byebug (~> 11.1.3) dotenv (~> 2.8.1) + eventmachine (~> 1.2.7) + faye-websocket (~> 0.11.3) rake (~> 13.2) rspec (~> 3.13) rubocop (~> 1.50.2) diff --git a/lib/openai.rb b/lib/openai.rb index 0fad1b2e..45b168ee 100644 --- a/lib/openai.rb +++ b/lib/openai.rb @@ -18,6 +18,7 @@ require_relative "openai/audio" require_relative "openai/version" require_relative "openai/batches" +require_relative "openai/real_time" module OpenAI class Error < StandardError; end @@ -46,11 +47,13 @@ class Configuration :log_errors, :organization_id, :uri_base, + :websocket_uri_base, :request_timeout, :extra_headers DEFAULT_API_VERSION = "v1".freeze DEFAULT_URI_BASE = "https://api.openai.com/".freeze + DEFAULT_WEBSOCKET_URI_BASE = "wss://api.openai.com/".freeze DEFAULT_REQUEST_TIMEOUT = 120 DEFAULT_LOG_ERRORS = false @@ -61,6 +64,7 @@ def initialize @log_errors = DEFAULT_LOG_ERRORS @organization_id = nil @uri_base = DEFAULT_URI_BASE + @websocket_uri_base = DEFAULT_WEBSOCKET_URI_BASE @request_timeout = DEFAULT_REQUEST_TIMEOUT @extra_headers = {} end diff --git a/lib/openai/client.rb b/lib/openai/client.rb index 1a641dde..8a798dbf 100644 --- a/lib/openai/client.rb +++ b/lib/openai/client.rb @@ -10,6 +10,7 @@ class Client log_errors organization_id uri_base + websocket_uri_base request_timeout extra_headers ].freeze @@ -95,6 +96,10 @@ def batches @batches ||= OpenAI::Batches.new(client: self) end + def real_time + @real_time ||= OpenAI::RealTime.new(client: self) + end + def moderations(parameters: {}) json_post(path: "/moderations", parameters: parameters) end diff --git a/lib/openai/http_headers.rb b/lib/openai/http_headers.rb index 62e3e74e..8f4b72c5 100644 --- a/lib/openai/http_headers.rb +++ b/lib/openai/http_headers.rb @@ -22,6 +22,13 @@ def openai_headers }.compact end + def openai_realtime_headers + { + "Authorization" => "Bearer #{@client.access_token}", + "OpenAI-Beta" => "realtime=v1" + } + end + def azure_headers { "Content-Type" => "application/json", diff --git a/lib/openai/real_time.rb b/lib/openai/real_time.rb new file mode 100644 index 00000000..b7bddf10 --- /dev/null +++ b/lib/openai/real_time.rb @@ -0,0 +1,31 @@ +require "faye/websocket" +require "eventmachine" + +module OpenAI + class RealTime + include HTTPHeaders + + def initialize(client:) + @client = client + @websocket = nil + @on_message = nil + end + + def on_message(&block) + @on_message = block + end + + def connect(model: "gpt-4o-realtime-preview-2024-10-01") + uri = "#{File.join(@client.websocket_uri_base, @client.api_version, 'realtime')}?model=#{model}" + + EM.run do + @websocket = Faye::WebSocket::Client.new(uri, nil, headers: openai_realtime_headers) + @websocket.on :message, @on_message + end + end + + def send_event(event) + @websocket.send(event) + end + end +end