openai-tools 3.1.0

Tools for OpenAI API
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
//! WebSocket client and active-session types for the OpenAI Live API.

use std::sync::{
    atomic::{AtomicBool, Ordering},
    Arc,
};

use base64::prelude::*;
use futures_util::{stream::SplitSink, stream::SplitStream, SinkExt, StreamExt};
use tokio::{net::TcpStream, sync::Mutex};
use tokio_tungstenite::{
    connect_async_with_config,
    tungstenite::{client::IntoClientRequest, Message as WsMessage},
    MaybeTlsStream, WebSocketStream,
};

use crate::common::{
    auth::{AuthProvider, OpenAIAuth},
    errors::{OpenAIToolError, Result},
    models::LiveModel,
};

use super::{
    events::{ClientEvent, ServerEvent, SessionClosedEvent, SessionResource},
    session::{
        Delegation, LiveAudioConfig, LiveAudioEncoding, LiveAudioFormat, LiveAudioOutputConfig, LiveSessionConfig, LiveSessionUpdate, LiveVoice,
    },
};

const LIVE_SESSIONS_URL: &str = "wss://api.openai.com/v1/live/sessions";

type LiveWebSocket = WebSocketStream<MaybeTlsStream<TcpStream>>;
type LiveSink = SplitSink<LiveWebSocket, WsMessage>;
type LiveStream = SplitStream<LiveWebSocket>;

/// Builder for a primary or forked Live WebSocket session.
#[derive(Debug, Clone)]
pub struct LiveClient {
    auth: AuthProvider,
    session_config: LiveSessionConfig,
}

impl LiveClient {
    /// Creates a client using `OPENAI_API_KEY`.
    pub fn new() -> Self {
        let auth = AuthProvider::openai_from_env().expect("OPENAI_API_KEY must be set");
        Self::with_auth(auth)
    }

    /// Creates a client using an explicit OpenAI API key.
    pub fn with_api_key(api_key: impl Into<String>) -> Self {
        Self::with_auth(AuthProvider::OpenAI(OpenAIAuth::new(api_key)))
    }

    /// Creates a client with an authentication provider.
    ///
    /// The Live API currently supports only [`AuthProvider::OpenAI`]. Passing
    /// Azure authentication is allowed here so that configuration can be
    /// shared, but [`connect`](Self::connect) and [`fork`](Self::fork) reject it.
    pub fn with_auth(auth: AuthProvider) -> Self {
        Self { auth, session_config: LiveSessionConfig::default() }
    }

    /// Selects the frontend Live model.
    pub fn model(&mut self, model: LiveModel) -> &mut Self {
        self.session_config.model = Some(model);
        self
    }

    /// Sets immutable frontend instructions.
    pub fn instructions(&mut self, instructions: impl Into<String>) -> &mut Self {
        self.session_config.instructions = Some(instructions.into());
        self
    }

    /// Selects the output voice.
    pub fn voice(&mut self, voice: LiveVoice) -> &mut Self {
        self.audio_mut().output.get_or_insert_with(LiveAudioOutputConfig::default).voice = Some(voice);
        self
    }

    /// Selects the common input/output audio format.
    pub fn audio_format(&mut self, format: LiveAudioFormat) -> &mut Self {
        self.audio_mut().format = Some(format);
        self
    }

    /// Selects the delegation strategy.
    pub fn delegation(&mut self, delegation: Delegation) -> &mut Self {
        self.session_config.delegation = Some(delegation);
        self
    }

    /// Enables or disables storage for later forks.
    pub fn store(&mut self, store: bool) -> &mut Self {
        self.session_config.store = Some(store);
        self
    }

    /// Supplies initial Responses-style input items.
    pub fn input(&mut self, input: Vec<serde_json::Value>) -> &mut Self {
        self.session_config.input = Some(input);
        self
    }

    /// Starts a new primary Live WebSocket session.
    pub async fn connect(&self) -> Result<LiveSession> {
        let mut config = self.session_config.clone();
        config.model.get_or_insert_default();
        self.connect_to(LIVE_SESSIONS_URL.to_string(), config).await
    }

    /// Forks a stored Live session, applying only explicitly configured fields.
    pub async fn fork(&self, session_id: &str) -> Result<LiveSession> {
        let url = format!("{LIVE_SESSIONS_URL}/{session_id}/fork");
        self.connect_to(url, self.session_config.clone()).await
    }

    fn audio_mut(&mut self) -> &mut LiveAudioConfig {
        self.session_config.audio.get_or_insert_with(LiveAudioConfig::default)
    }

    async fn connect_to(&self, url: String, config: LiveSessionConfig) -> Result<LiveSession> {
        let api_key = match &self.auth {
            AuthProvider::OpenAI(auth) => auth.api_key(),
            AuthProvider::Azure(_) => {
                return Err(OpenAIToolError::Error("Azure authentication is not supported by the OpenAI Live API".to_string()));
            }
        };

        let mut request =
            url.into_client_request().map_err(|error| OpenAIToolError::Error(format!("Failed to build Live WebSocket request: {error}")))?;
        request.headers_mut().insert(
            "Authorization",
            format!("Bearer {api_key}").parse().map_err(|error| OpenAIToolError::Error(format!("Invalid Authorization header: {error}")))?,
        );

        let (socket, _) = connect_async_with_config(request, None, false)
            .await
            .map_err(|error| OpenAIToolError::WebSocketError(format!("Live connection failed: {error}")))?;
        let (sink, stream) = socket.split();
        let sender = LiveSender::new(sink);
        let mut receiver = LiveReceiver::new(sender.clone(), stream);

        sender.send(ClientEvent::SessionStart { event_id: None, session: config }).await?;

        let started = loop {
            match receiver.recv().await? {
                Some(ServerEvent::SessionStarted(event)) => break event.session,
                Some(ServerEvent::Error(event)) => {
                    return Err(OpenAIToolError::Error(format!("Live session start failed: {} - {}", event.error.code, event.error.message)));
                }
                Some(_) => continue,
                None => {
                    return Err(OpenAIToolError::Error("Live WebSocket closed before session.started".to_string()));
                }
            }
        };

        let pcm16 =
            started.audio.as_ref().and_then(|audio| audio.format.as_ref()).is_none_or(|format| matches!(format.encoding, LiveAudioEncoding::Pcm));
        sender.pcm16.store(pcm16, Ordering::Relaxed);

        Ok(LiveSession { started, sender, receiver })
    }
}

impl Default for LiveClient {
    fn default() -> Self {
        Self::new()
    }
}

/// Sending half of a split Live session.
#[derive(Clone)]
pub struct LiveSender {
    sink: Arc<Mutex<LiveSink>>,
    pcm16: Arc<AtomicBool>,
}

impl LiveSender {
    fn new(sink: LiveSink) -> Self {
        Self { sink: Arc::new(Mutex::new(sink)), pcm16: Arc::new(AtomicBool::new(true)) }
    }

    /// Sends a raw typed client event.
    pub async fn send(&self, event: ClientEvent) -> Result<()> {
        let json = serde_json::to_string(&event)?;
        self.send_message(WsMessage::Text(json.into())).await
    }

    /// Appends base64-encoded audio input.
    pub async fn append_audio(&self, base64: &str) -> Result<()> {
        self.send(ClientEvent::InputAudioAppend { event_id: None, audio: base64.to_string() }).await
    }

    /// Base64-encodes and appends raw audio input.
    ///
    /// PCM input must contain complete signed 16-bit samples, so odd byte
    /// lengths are rejected before anything is sent.
    pub async fn append_audio_bytes(&self, audio: &[u8]) -> Result<()> {
        if self.pcm16.load(Ordering::Relaxed) && !audio.len().is_multiple_of(2) {
            return Err(OpenAIToolError::Error("PCM audio must contain an even number of bytes (16-bit sample boundary)".to_string()));
        }
        self.append_audio(&BASE64_STANDARD.encode(audio)).await
    }

    /// Mutes Live input audio.
    pub async fn mute(&self) -> Result<()> {
        self.send(ClientEvent::InputAudioMute { event_id: None }).await
    }

    /// Unmutes Live input audio.
    pub async fn unmute(&self) -> Result<()> {
        self.send(ClientEvent::InputAudioUnmute { event_id: None }).await
    }

    /// Appends system-level instructions.
    pub async fn append_instructions(&self, content: impl Into<String>, delegation_id: Option<&str>) -> Result<()> {
        self.send(ClientEvent::InstructionsAppend { event_id: None, content: content.into(), delegation_id: delegation_id.map(str::to_string) }).await
    }

    /// Appends silent context for model reasoning.
    pub async fn append_thinking(&self, content: impl Into<String>, delegation_id: Option<&str>) -> Result<()> {
        self.send(ClientEvent::ThinkingAppend { event_id: None, content: content.into(), delegation_id: delegation_id.map(str::to_string) }).await
    }

    /// Appends content that the model should speak.
    pub async fn append_commentary(&self, content: impl Into<String>, delegation_id: Option<&str>) -> Result<()> {
        self.send(ClientEvent::CommentaryAppend { event_id: None, content: content.into(), delegation_id: delegation_id.map(str::to_string) }).await
    }

    /// Adds an input item to a Responses delegation.
    pub async fn create_response_item(&self, item: serde_json::Value) -> Result<()> {
        self.send(ClientEvent::ResponseItemCreate { event_id: None, item }).await
    }

    /// Submits a function-call result to a Responses delegation.
    pub async fn submit_function_output(&self, call_id: impl Into<String>, output: impl Into<String>) -> Result<()> {
        self.create_response_item(serde_json::json!({
            "type": "function_call_output",
            "call_id": call_id.into(),
            "output": output.into(),
        }))
        .await
    }

    /// Requests that a Responses delegation continue generating.
    pub async fn create_response(&self) -> Result<()> {
        self.send(ClientEvent::ResponseCreate { event_id: None }).await
    }

    /// Updates settings within the current delegation type.
    pub async fn update_delegation(&self, delegation: Delegation) -> Result<()> {
        self.send(ClientEvent::SessionUpdate { event_id: None, session: LiveSessionUpdate { delegation } }).await
    }

    async fn send_message(&self, message: WsMessage) -> Result<()> {
        self.sink.lock().await.send(message).await.map_err(|error| OpenAIToolError::WebSocketError(format!("Failed to send Live event: {error}")))
    }

    async fn close_transport(&self) -> Result<()> {
        self.sink.lock().await.close().await.map_err(|error| OpenAIToolError::WebSocketError(format!("Failed to close Live WebSocket: {error}")))
    }
}

/// Receiving half of a split Live session.
pub struct LiveReceiver {
    sender: LiveSender,
    stream: LiveStream,
}

impl LiveReceiver {
    fn new(sender: LiveSender, stream: LiveStream) -> Self {
        Self { sender, stream }
    }

    /// Receives the next typed server event.
    ///
    /// Returns `None` after a WebSocket Close frame or end of stream. Ping
    /// frames are answered with Pong and are not surfaced to the caller.
    pub async fn recv(&mut self) -> Result<Option<ServerEvent>> {
        loop {
            match self.stream.next().await {
                Some(Ok(WsMessage::Text(text))) => return Ok(Some(serde_json::from_str(&text)?)),
                Some(Ok(WsMessage::Close(_))) | None => return Ok(None),
                Some(Ok(WsMessage::Ping(payload))) => {
                    self.sender.send_message(WsMessage::Pong(payload)).await?;
                }
                Some(Ok(_)) => {}
                Some(Err(error)) => {
                    return Err(OpenAIToolError::WebSocketError(format!("Live WebSocket receive failed: {error}")));
                }
            }
        }
    }

    /// Gracefully closes the Live session and then the WebSocket.
    pub async fn close(mut self) -> Result<Option<SessionClosedEvent>> {
        if let Err(error) = self.sender.send(ClientEvent::SessionClose { event_id: None }).await {
            let _ = self.sender.close_transport().await;
            return Err(error);
        }

        let closed = loop {
            match self.recv().await {
                Ok(Some(ServerEvent::SessionClosed(event))) => break Ok(Some(event)),
                Ok(Some(_)) => continue,
                Ok(None) => break Ok(None),
                Err(error) => break Err(error),
            }
        };

        let transport_close = self.sender.close_transport().await;
        match closed {
            Ok(event) => {
                transport_close?;
                Ok(event)
            }
            Err(error) => {
                let _ = transport_close;
                Err(error)
            }
        }
    }
}

/// An active primary or forked Live WebSocket session.
///
/// The model advances only while input audio is streaming in real time. Keep
/// sending audio frames (silence is acceptable) even when injecting commentary
/// or instructions; otherwise the injection can remain incomplete until close.
pub struct LiveSession {
    started: SessionResource,
    sender: LiveSender,
    receiver: LiveReceiver,
}

impl LiveSession {
    /// Returns the opaque Live session identifier.
    pub fn session_id(&self) -> &str {
        &self.started.id
    }

    /// Returns the full resolved session from `session.started`.
    pub fn started(&self) -> &SessionResource {
        &self.started
    }

    /// Splits the session for concurrent sending and receiving.
    pub fn split(self) -> (LiveSender, LiveReceiver) {
        (self.sender, self.receiver)
    }

    /// Sends a raw typed client event.
    pub async fn send(&self, event: ClientEvent) -> Result<()> {
        self.sender.send(event).await
    }

    /// Receives the next typed server event.
    pub async fn recv(&mut self) -> Result<Option<ServerEvent>> {
        self.receiver.recv().await
    }

    /// Appends base64-encoded audio input.
    pub async fn append_audio(&self, base64: &str) -> Result<()> {
        self.sender.append_audio(base64).await
    }

    /// Base64-encodes and appends raw audio input.
    pub async fn append_audio_bytes(&self, audio: &[u8]) -> Result<()> {
        self.sender.append_audio_bytes(audio).await
    }

    /// Mutes input audio.
    pub async fn mute(&self) -> Result<()> {
        self.sender.mute().await
    }

    /// Unmutes input audio.
    pub async fn unmute(&self) -> Result<()> {
        self.sender.unmute().await
    }

    /// Appends system-level instructions.
    pub async fn append_instructions(&self, content: impl Into<String>, delegation_id: Option<&str>) -> Result<()> {
        self.sender.append_instructions(content, delegation_id).await
    }

    /// Appends silent reasoning context.
    pub async fn append_thinking(&self, content: impl Into<String>, delegation_id: Option<&str>) -> Result<()> {
        self.sender.append_thinking(content, delegation_id).await
    }

    /// Appends content that the model should speak.
    pub async fn append_commentary(&self, content: impl Into<String>, delegation_id: Option<&str>) -> Result<()> {
        self.sender.append_commentary(content, delegation_id).await
    }

    /// Adds an input item to a Responses delegation.
    pub async fn create_response_item(&self, item: serde_json::Value) -> Result<()> {
        self.sender.create_response_item(item).await
    }

    /// Submits a function-call result to a Responses delegation.
    pub async fn submit_function_output(&self, call_id: impl Into<String>, output: impl Into<String>) -> Result<()> {
        self.sender.submit_function_output(call_id, output).await
    }

    /// Requests that a Responses delegation continue generating.
    pub async fn create_response(&self) -> Result<()> {
        self.sender.create_response().await
    }

    /// Updates settings within the current delegation type.
    pub async fn update_delegation(&self, delegation: Delegation) -> Result<()> {
        self.sender.update_delegation(delegation).await
    }

    /// Sends `session.close`, waits for `session.closed`, and closes the socket.
    pub async fn close(self) -> Result<Option<SessionClosedEvent>> {
        self.receiver.close().await
    }
}