Skip to content

Commit 7b3d0d8

Browse files
authored
Realtime API types + example (64bit#276)
* add types for realtime; enable it and its dependency with 'realtime' crate feature * add realtime example * doc for realtime feature * cleanup * emtpy comment: cargo clippy * update README * fix example readme
1 parent e8158e5 commit 7b3d0d8

File tree

18 files changed

+1372
-1
lines changed

18 files changed

+1372
-1
lines changed

async-openai/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ rustls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"]
2222
native-tls = ["reqwest/native-tls"]
2323
# Remove dependency on OpenSSL
2424
native-tls-vendored = ["reqwest/native-tls-vendored"]
25+
realtime = ["dep:tokio-tungstenite"]
2526

2627
[dependencies]
2728
backoff = { version = "0.4.0", features = ["tokio"] }
@@ -46,6 +47,11 @@ async-convert = "1.0.0"
4647
secrecy = { version = "0.8.0", features = ["serde"] }
4748
bytes = "1.6.0"
4849
eventsource-stream = "0.2.3"
50+
tokio-tungstenite = { version = "0.24.0", optional = true, default-features = false }
4951

5052
[dev-dependencies]
5153
tokio-test = "0.4.4"
54+
55+
[package.metadata.docs.rs]
56+
all-features = true
57+
rustdoc-args = ["--cfg", "docsrs"]

async-openai/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
- [x] Models
3636
- [x] Moderations
3737
- [ ] Organizations | Administration
38+
- [x] Realtime API types (Beta)
3839
- [ ] Uploads
3940
- SSE streaming on available APIs
4041
- Requests (except SSE streaming) including form submissions are retried with exponential backoff when [rate limited](https://platform.openai.com/docs/guides/rate-limits).
@@ -58,6 +59,11 @@ $Env:OPENAI_API_KEY='sk-...'
5859
- Visit [examples](https://github.com/64bit/async-openai/tree/main/examples) directory on how to use `async-openai`.
5960
- Visit [docs.rs/async-openai](https://docs.rs/async-openai) for docs.
6061

62+
## Realtime API
63+
64+
Only types for Realtime API are imlemented, and can be enabled with feature flag `realtime`
65+
These types may change when OpenAI releases official specs for them.
66+
6167
## Image Generation Example
6268

6369
```rust

async-openai/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
//! ## Examples
7777
//! For full working examples for all supported features see [examples](https://github.com/64bit/async-openai/tree/main/examples) directory in the repository.
7878
//!
79+
#![cfg_attr(docsrs, feature(doc_cfg))]
7980
mod assistant_files;
8081
mod assistants;
8182
mod audio;

async-openai/src/types/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ mod message;
1717
mod message_file;
1818
mod model;
1919
mod moderation;
20+
#[cfg_attr(docsrs, doc(cfg(feature = "realtime")))]
21+
#[cfg(feature = "realtime")]
22+
pub mod realtime;
2023
mod run;
2124
mod step;
2225
mod thread;
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
use serde::{Deserialize, Serialize};
2+
use tokio_tungstenite::tungstenite::Message;
3+
4+
use super::{item::Item, session_resource::SessionResource};
5+
6+
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
7+
pub struct SessionUpdateEvent {
8+
/// Optional client-generated ID used to identify this event.
9+
#[serde(skip_serializing_if = "Option::is_none")]
10+
pub event_id: Option<String>,
11+
/// Session configuration to update.
12+
pub session: SessionResource,
13+
}
14+
15+
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
16+
pub struct InputAudioBufferAppendEvent {
17+
/// Optional client-generated ID used to identify this event.
18+
#[serde(skip_serializing_if = "Option::is_none")]
19+
pub event_id: Option<String>,
20+
/// Base64-encoded audio bytes.
21+
pub audio: String,
22+
}
23+
24+
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
25+
pub struct InputAudioBufferCommitEvent {
26+
/// Optional client-generated ID used to identify this event.
27+
#[serde(skip_serializing_if = "Option::is_none")]
28+
pub event_id: Option<String>,
29+
}
30+
31+
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
32+
pub struct InputAudioBufferClearEvent {
33+
/// Optional client-generated ID used to identify this event.
34+
#[serde(skip_serializing_if = "Option::is_none")]
35+
pub event_id: Option<String>,
36+
}
37+
38+
#[derive(Debug, Serialize, Deserialize, Clone)]
39+
pub struct ConversationItemCreateEvent {
40+
/// Optional client-generated ID used to identify this event.
41+
#[serde(skip_serializing_if = "Option::is_none")]
42+
pub event_id: Option<String>,
43+
44+
/// The ID of the preceding item after which the new item will be inserted.
45+
#[serde(skip_serializing_if = "Option::is_none")]
46+
pub previous_item_id: Option<String>,
47+
48+
/// The item to add to the conversation.
49+
pub item: Item,
50+
}
51+
52+
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
53+
pub struct ConversationItemTruncateEvent {
54+
/// Optional client-generated ID used to identify this event.
55+
#[serde(skip_serializing_if = "Option::is_none")]
56+
pub event_id: Option<String>,
57+
58+
/// The ID of the assistant message item to truncate.
59+
pub item_id: String,
60+
61+
/// The index of the content part to truncate.
62+
pub content_index: u32,
63+
64+
/// Inclusive duration up to which audio is truncated, in milliseconds.
65+
pub audio_end_ms: u32,
66+
}
67+
68+
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
69+
pub struct ConversationItemDeleteEvent {
70+
/// Optional client-generated ID used to identify this event.
71+
#[serde(skip_serializing_if = "Option::is_none")]
72+
pub event_id: Option<String>,
73+
74+
/// The ID of the item to delete.
75+
pub item_id: String,
76+
}
77+
78+
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
79+
pub struct ResponseCreateEvent {
80+
/// Optional client-generated ID used to identify this event.
81+
#[serde(skip_serializing_if = "Option::is_none")]
82+
pub event_id: Option<String>,
83+
84+
/// Configuration for the response.
85+
pub response: Option<SessionResource>,
86+
}
87+
88+
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
89+
pub struct ResponseCancelEvent {
90+
/// Optional client-generated ID used to identify this event.
91+
#[serde(skip_serializing_if = "Option::is_none")]
92+
pub event_id: Option<String>,
93+
}
94+
95+
/// These are events that the OpenAI Realtime WebSocket server will accept from the client.
96+
#[derive(Debug, Serialize, Deserialize)]
97+
#[serde(tag = "type")]
98+
pub enum ClientEvent {
99+
/// Send this event to update the session’s default configuration.
100+
#[serde(rename = "session.update")]
101+
SessionUpdate(SessionUpdateEvent),
102+
103+
/// Send this event to append audio bytes to the input audio buffer.
104+
#[serde(rename = "input_audio_buffer.append")]
105+
InputAudioBufferAppend(InputAudioBufferAppendEvent),
106+
107+
/// Send this event to commit audio bytes to a user message.
108+
#[serde(rename = "input_audio_buffer.commit")]
109+
InputAudioBufferCommit(InputAudioBufferCommitEvent),
110+
111+
/// Send this event to clear the audio bytes in the buffer.
112+
#[serde(rename = "input_audio_buffer.clear")]
113+
InputAudioBufferClear(InputAudioBufferClearEvent),
114+
115+
/// Send this event when adding an item to the conversation.
116+
#[serde(rename = "conversation.item.create")]
117+
ConversationItemCreate(ConversationItemCreateEvent),
118+
119+
/// Send this event when you want to truncate a previous assistant message’s audio.
120+
#[serde(rename = "conversation.item.truncate")]
121+
ConversationItemTruncate(ConversationItemTruncateEvent),
122+
123+
/// Send this event when you want to remove any item from the conversation history.
124+
#[serde(rename = "conversation.item.delete")]
125+
ConversationItemDelete(ConversationItemDeleteEvent),
126+
127+
/// Send this event to trigger a response generation.
128+
#[serde(rename = "response.create")]
129+
ResponseCreate(ResponseCreateEvent),
130+
131+
/// Send this event to cancel an in-progress response.
132+
#[serde(rename = "response.cancel")]
133+
ResponseCancel(ResponseCancelEvent),
134+
}
135+
136+
impl From<&ClientEvent> for String {
137+
fn from(value: &ClientEvent) -> Self {
138+
serde_json::to_string(value).unwrap()
139+
}
140+
}
141+
142+
impl From<ClientEvent> for Message {
143+
fn from(value: ClientEvent) -> Self {
144+
Message::Text(String::from(&value))
145+
}
146+
}
147+
148+
macro_rules! message_from_event {
149+
($from_typ:ty, $evt_typ:ty) => {
150+
impl From<$from_typ> for Message {
151+
fn from(value: $from_typ) -> Self {
152+
Self::from(<$evt_typ>::from(value))
153+
}
154+
}
155+
};
156+
}
157+
158+
macro_rules! event_from {
159+
($from_typ:ty, $evt_typ:ty, $variant:ident) => {
160+
impl From<$from_typ> for $evt_typ {
161+
fn from(value: $from_typ) -> Self {
162+
<$evt_typ>::$variant(value)
163+
}
164+
}
165+
};
166+
}
167+
168+
event_from!(SessionUpdateEvent, ClientEvent, SessionUpdate);
169+
event_from!(
170+
InputAudioBufferAppendEvent,
171+
ClientEvent,
172+
InputAudioBufferAppend
173+
);
174+
event_from!(
175+
InputAudioBufferCommitEvent,
176+
ClientEvent,
177+
InputAudioBufferCommit
178+
);
179+
event_from!(
180+
InputAudioBufferClearEvent,
181+
ClientEvent,
182+
InputAudioBufferClear
183+
);
184+
event_from!(
185+
ConversationItemCreateEvent,
186+
ClientEvent,
187+
ConversationItemCreate
188+
);
189+
event_from!(
190+
ConversationItemTruncateEvent,
191+
ClientEvent,
192+
ConversationItemTruncate
193+
);
194+
event_from!(
195+
ConversationItemDeleteEvent,
196+
ClientEvent,
197+
ConversationItemDelete
198+
);
199+
event_from!(ResponseCreateEvent, ClientEvent, ResponseCreate);
200+
event_from!(ResponseCancelEvent, ClientEvent, ResponseCancel);
201+
202+
message_from_event!(SessionUpdateEvent, ClientEvent);
203+
message_from_event!(InputAudioBufferAppendEvent, ClientEvent);
204+
message_from_event!(InputAudioBufferCommitEvent, ClientEvent);
205+
message_from_event!(InputAudioBufferClearEvent, ClientEvent);
206+
message_from_event!(ConversationItemCreateEvent, ClientEvent);
207+
message_from_event!(ConversationItemTruncateEvent, ClientEvent);
208+
message_from_event!(ConversationItemDeleteEvent, ClientEvent);
209+
message_from_event!(ResponseCreateEvent, ClientEvent);
210+
message_from_event!(ResponseCancelEvent, ClientEvent);
211+
212+
impl From<Item> for ConversationItemCreateEvent {
213+
fn from(value: Item) -> Self {
214+
Self {
215+
event_id: None,
216+
previous_item_id: None,
217+
item: value,
218+
}
219+
}
220+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
#[derive(Debug, Serialize, Deserialize, Clone)]
4+
#[serde(tag = "type")]
5+
pub enum ContentPart {
6+
#[serde(rename = "text")]
7+
Text {
8+
/// The text content
9+
text: String,
10+
},
11+
#[serde(rename = "audio")]
12+
Audio {
13+
/// Base64-encoded audio data
14+
audio: Option<String>,
15+
/// The transcript of the audio
16+
transcript: String,
17+
},
18+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
#[derive(Debug, Serialize, Deserialize, Clone)]
4+
pub struct Conversation {
5+
/// The unique ID of the conversation.
6+
pub id: String,
7+
8+
/// The object type, must be "realtime.conversation".
9+
pub object: String,
10+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
#[derive(Debug, Serialize, Deserialize, Clone)]
4+
pub struct RealtimeAPIError {
5+
/// The type of error (e.g., "invalid_request_error", "server_error").
6+
pub r#type: String,
7+
8+
/// Error code, if any.
9+
pub code: Option<String>,
10+
11+
/// A human-readable error message.
12+
pub message: String,
13+
14+
/// Parameter related to the error, if any.
15+
pub param: Option<String>,
16+
17+
/// The event_id of the client event that caused the error, if applicable.
18+
pub event_id: Option<String>,
19+
}

0 commit comments

Comments
 (0)