Skip to content

Commit 3d3f112

Browse files
authored
feat: all native features on wasm (#541)
* MaybeSend * StreamResponse helper type * all types udpated with StreamResponse for wasm * api groups updated with StreamResponse * remove redundant OpenAIError wasm/non-wasm * updated doc in lib.rs * updated readme * update to macro crate * client and cargo.toml for wasm streaming * remove duplicated methods for wasm * deduplicate build_request_factory_with_form using MaybeSend trait * remove duplicate implementation for streaming * unify stream * add wasm streaming example * dedup build_request_factory_with_form * make AsyncTryFrom use MaybeSend * cleanup MiddlewareInput * bump macro version * update docs * cleanup docs * doc comments * update doc * simplify dynamic dispatch docs
1 parent f5ede7a commit 3d3f112

41 files changed

Lines changed: 418 additions & 348 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

async-openai-macros/Cargo.toml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "async-openai-macros"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
authors = ["Himanshu Neema"]
55
keywords = ["openai", "macros", "ai"]
66
description = "Macros for async-openai"
@@ -14,9 +14,6 @@ readme = "README.md"
1414
[lib]
1515
proc-macro = true
1616

17-
[features]
18-
middleware = []
19-
2017
[dependencies]
2118
syn = { version = "2.0", features = ["full"] }
2219
quote = "1.0"

async-openai-macros/src/lib.rs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use proc_macro::TokenStream;
22
use quote::{quote, ToTokens};
33
use syn::{
44
parse::{Parse, ParseStream},
5-
parse_macro_input, parse_quote,
5+
parse_macro_input,
66
punctuated::Punctuated,
77
token::Comma,
88
FnArg, GenericParam, Generics, ItemFn, Pat, PatType, TypeParam, WhereClause,
@@ -57,7 +57,6 @@ pub fn byot(args: TokenStream, item: TokenStream) -> TokenStream {
5757
let input = parse_macro_input!(item as ItemFn);
5858
let mut new_generics = Generics::default();
5959
let mut param_count = 0;
60-
let middleware_enabled = cfg!(feature = "middleware");
6160

6261
// Process function arguments
6362
let mut new_params = Vec::new();
@@ -83,18 +82,6 @@ pub fn byot(args: TokenStream, item: TokenStream) -> TokenStream {
8382
{
8483
type_param.bounds.extend(vec![bound.clone()]);
8584
}
86-
let needs_middleware_replay_bounds =
87-
bounds_args.bounds.iter().any(|(name, bound)| {
88-
name == &generic_name
89-
&& bound.to_token_stream().to_string().contains("Clone")
90-
&& !bound.to_token_stream().to_string().contains("Display")
91-
});
92-
if middleware_enabled && needs_middleware_replay_bounds {
93-
type_param
94-
.bounds
95-
.push(parse_quote!(crate::middleware::MiddlewareInput));
96-
}
97-
9885
new_params.push(GenericParam::Type(type_param));
9986
param_count += 1;
10087
quote! { #pat_ident: #generic_ident }
@@ -136,7 +123,7 @@ pub fn byot(args: TokenStream, item: TokenStream) -> TokenStream {
136123

137124
// Generate return type based on stream flag
138125
let return_type = if bounds_args.stream {
139-
quote! { Result<::std::pin::Pin<Box<dyn ::futures::Stream<Item = Result<R, OpenAIError>> + Send>>, OpenAIError> }
126+
quote! { Result<crate::types::stream::StreamResponse<R>, OpenAIError> }
140127
} else {
141128
quote! { Result<R, OpenAIError> }
142129
};

async-openai/Cargo.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ chat-completion = ["chat-completion-types", "_api"]
4949
assistant = ["assistant-types", "_api", ]
5050
administration = ["administration-types", "_api"]
5151
completions = ["completion-types", "_api"]
52-
middleware = ["dep:tower", "_api", "async-openai-macros?/middleware"]
52+
middleware = ["dep:tower", "_api"]
5353

5454
# Type feature flags - these enable only the types
5555
response-types = ["dep:derive_builder"]
@@ -162,8 +162,9 @@ bytes = { version = "1.11", optional = true }
162162

163163
# API dependencies - only needed when API features are enabled
164164
# We use a feature gate to enable these when any API feature is enabled
165-
async-openai-macros = { path = "../async-openai-macros", version = "0.2.0", optional = true }
165+
async-openai-macros = { path = "../async-openai-macros", version = "0.3.0", optional = true }
166166
base64 = { version = "0.22", optional = true }
167+
futures = { version = "0.3", optional = true }
167168
rand = { version = "0.9", optional = true }
168169
reqwest = { version = "0.13", features = [
169170
"json",
@@ -177,18 +178,17 @@ secrecy = { version = "0.10", features = ["serde"], optional = true }
177178
serde_urlencoded = { version = "0.7", optional = true }
178179
url = { version = "2.5", optional = true }
179180
tower = { version = "0.5", features = ["limit", "retry", "timeout", "util"], optional = true }
181+
eventsource-stream = { version = "0.2", optional = true }
180182
## For Webhook signature verification
181183
hmac = { version = "0.12", optional = true, default-features = false}
182184
sha2 = { version = "0.10", optional = true, default-features = false }
183185
hex = { version = "0.4", optional = true, default-features = false }
184186

185-
## API Non-WASM dependencies (streaming and retry is not implemented for WASM yet)
187+
## API Non-WASM dependencies
186188
[target.'cfg(not(target_family = "wasm"))'.dependencies]
187-
futures = { version = "0.3", optional = true }
188189
tokio = { version = "1", features = ["fs", "macros"], optional = true }
189190
tokio-stream = { version = "0.1", optional = true }
190191
tokio-util = { version = "0.7", features = ["codec", "io-util"], optional = true }
191-
eventsource-stream = { version = "0.2", optional = true }
192192
## For Realtime websocket
193193
tokio-tungstenite = { version = "0.28", optional = true, default-features = false }
194194

async-openai/MIDDLEWARE.md

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -91,22 +91,17 @@ Custom tower retry policies can call `middleware::retry::should_retry` to reuse
9191

9292
On native targets retries wait using `tokio::time::sleep`. On WASM retries are immediate.
9393

94-
## Native and WASM bounds
95-
96-
The conceptual middleware boundary stays the same; only the platform thread-safety bounds differ.
94+
## Error Handling
9795

98-
On native targets, middleware services installed with `Client::with_http_service` must be `Send + Sync + 'static` and return `Send + 'static` futures.
96+
`OpenAIError::Boxed` is available only when the `middleware` feature is enabled.
9997

100-
On WASM targets, middleware services and futures must be `'static`.
98+
Custom middleware services installed with `Client::with_http_service` may use any error type that implements `Into<OpenAIError>`. This lets middleware preserve structured errors when it has a dedicated `OpenAIError` conversion.
10199

102-
## Bring Your Own Types Interaction
100+
Tower's `BoxError` converts into `OpenAIError::Boxed`, which is useful for generic tower layers whose concrete error type is erased. Callers can still downcast the boxed error when they know the original error type.
103101

104-
With the `byot` feature, generated `*_byot` methods keep minimal trait bounds. When `middleware` feature is enabled additional `MiddlewareInput` bounds are added based on native or WASM targets so the input can be stored long enough to rebuild a fresh request for retries.
105102

106-
## Error Handling
103+
## Bring Your Own Types Interaction
107104

108-
`OpenAIError::Boxed` is available only when the `middleware` feature is enabled.
105+
With the `byot` feature, generated `*_byot` methods keep the same minimal trait bounds with or without middleware. JSON request bodies are serialized before they enter the replayable middleware request factory; multipart request bodies use the client-level replay bounds required by form handling.
109106

110-
Custom middleware services installed with `Client::with_http_service` may use any error type that implements `Into<OpenAIError>`. This lets middleware preserve structured errors when it has a dedicated `OpenAIError` conversion.
111107

112-
Tower's `BoxError` converts into `OpenAIError::Boxed`, which is useful for generic tower layers whose concrete error type is erased. Callers can still downcast the boxed error when they know the original error type.

async-openai/README.md

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919

2020
## Overview
2121

22-
`async-openai` is an unofficial Rust library for OpenAI, based on [OpenAI OpenAPI spec](https://github.com/openai/openai-openapi). It implements all APIs from the spec:
22+
`async-openai` is an unofficial Rust library for OpenAI, based on [OpenAI OpenAPI spec](https://github.com/openai/openai-openapi). It implements all APIs from the spec.
23+
24+
<details>
25+
<summary>Feature Flags</summary>
2326

2427
| What | APIs | Crate Feature Flags |
2528
|---|---|---|
@@ -36,16 +39,18 @@
3639
| **Administration** | Admin API Keys, Invites, Users, Groups, Roles, Role assignments, Projects, Project users, Project groups, Project service accounts, Project API keys, Project rate limits, Audit logs, Usage, Certificates | `administration` |
3740
| **Legacy** | Completions | `completions` |
3841

39-
Features that makes `async-openai` unique:
42+
</details>
43+
44+
4045
- Bring your own custom types for Request or Response objects.
41-
- SSE streaming on available APIs.
42-
- Customize path, query and headers per request; customize path and headers globally (for all requests).
43-
- Requests (except SSE streaming) including form submissions are retried with exponential backoff when [rate limited](https://platform.openai.com/docs/guides/rate-limits).
46+
- Requests are retried with exponential backoff when [rate limited](https://platform.openai.com/docs/guides/rate-limits).
4447
- Ergonomic builder pattern for all request objects.
45-
- Granular feature flags to enable any types or apis: good for faster compilation and crate reuse.
46-
- Microsoft Azure OpenAI Service (only for APIs matching OpenAI spec).
47-
- WASM (doesn't support streaming yet)
48-
- Middleware support with [tower](https://crates.io/crates/tower) ecosystem
48+
- SSE streaming.
49+
- Customize path, query and headers per request or for all requests.
50+
- Granular feature flags to enable any types or apis.
51+
- Microsoft Azure OpenAI Service.
52+
- WASM.
53+
- Middleware support with [tower](https://crates.io/crates/tower) ecosystem.
4954

5055
## Usage
5156

@@ -111,18 +116,10 @@ async fn main() -> Result<(), Box<dyn Error>> {
111116
<sub>Scaled up for README, actual size 256x256</sub>
112117
</div>
113118

114-
## Webhooks
115-
116-
Support for webhook includes event types, signature verification, and building webhook events from payloads.
117-
118119
## Bring Your Own Types
119120

120121
Enable methods whose input and outputs are generics with `byot` feature. It creates a new method with same name and `_byot` suffix.
121122

122-
`byot` requires trait bounds:
123-
- a request type (`fn` input parameter) needs to implement `serde::Serialize` or `std::fmt::Display` trait
124-
- a response type (`fn` ouput parameter) needs to implement `serde::de::DeserializeOwned` trait.
125-
126123
For example, to use `serde_json::Value` as request and response type:
127124
```rust
128125
let response: Value = client
@@ -147,9 +144,11 @@ let response: Value = client
147144
This can be useful in many scenarios:
148145
- To use this library with other OpenAI compatible APIs whose types don't exactly match OpenAI.
149146
- Extend existing types in this crate with new fields with `serde` (for example with `#[serde(flatten)]`).
150-
- To avoid verbose types.
147+
- To avoid typing verbose types.
151148
- To escape deserialization errors.
152149

150+
`*_byot` methods require same trait bounds as regular methods.
151+
153152
Visit [examples/bring-your-own-type](https://github.com/64bit/async-openai/tree/main/examples/bring-your-own-type)
154153
directory to learn more.
155154

@@ -217,8 +216,7 @@ client
217216

218217
This allows you to use same code (say a `fn`) to call APIs on different OpenAI-compatible providers.
219218

220-
For any struct that implements `Config` trait, wrap it in a smart pointer and cast the pointer to `dyn Config`
221-
trait object, then create a client with `Box` or `Arc` wrapped configuration.
219+
Create a client with `Box` or `Arc` wrapped configuration.
222220

223221
For example:
224222

@@ -237,6 +235,10 @@ fn chat_completion(client: &Client<Box<dyn Config>>) {
237235
}
238236
```
239237

238+
## Webhooks
239+
240+
Support for webhook includes event types, signature verification, and building webhook events from payloads.
241+
240242
## Middleware
241243

242244
Middleware is supported via Tower ecosystem, which can be enabled with `middleware` feature. See [middleware](https://github.com/64bit/async-openai/blob/main/async-openai/MIDDLEWARE.md) for more detail.

async-openai/src/assistants/runs.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use crate::{
99
Client, RequestOptions,
1010
};
1111

12-
#[cfg(not(target_family = "wasm"))]
1312
use crate::types::assistants::AssistantEventStream;
1413

1514
/// Represents an execution run on a thread.
@@ -53,12 +52,11 @@ impl<'c, C: Config> Runs<'c, C> {
5352
/// Create a run.
5453
///
5554
/// byot: You must ensure "stream: true" in serialized `request`
56-
#[cfg(not(target_family = "wasm"))]
5755
#[crate::byot(
5856
T0 = serde::Serialize,
5957
R = serde::de::DeserializeOwned,
6058
stream = "true",
61-
where_clause = "R: std::marker::Send + 'static + TryFrom<eventsource_stream::Event, Error = OpenAIError>"
59+
where_clause = "R: crate::traits::MaybeSend + 'static + TryFrom<eventsource_stream::Event, Error = OpenAIError>"
6260
)]
6361
#[allow(unused_mut)]
6462
pub async fn create_stream(
@@ -144,13 +142,12 @@ impl<'c, C: Config> Runs<'c, C> {
144142
}
145143

146144
/// byot: You must ensure "stream: true" in serialized `request`
147-
#[cfg(not(target_family = "wasm"))]
148145
#[crate::byot(
149146
T0 = std::fmt::Display,
150147
T1 = serde::Serialize,
151148
R = serde::de::DeserializeOwned,
152149
stream = "true",
153-
where_clause = "R: std::marker::Send + 'static + TryFrom<eventsource_stream::Event, Error = OpenAIError>"
150+
where_clause = "R: crate::traits::MaybeSend + 'static + TryFrom<eventsource_stream::Event, Error = OpenAIError>"
154151
)]
155152
#[allow(unused_mut)]
156153
pub async fn submit_tool_outputs_stream(

async-openai/src/assistants/threads.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use crate::{
88
Client, Messages, RequestOptions, Runs,
99
};
1010

11-
#[cfg(not(target_family = "wasm"))]
1211
use crate::types::assistants::AssistantEventStream;
1312

1413
/// Create threads that assistants can interact with.
@@ -54,12 +53,11 @@ impl<'c, C: Config> Threads<'c, C> {
5453
/// Create a thread and run it in one request (streaming).
5554
///
5655
/// byot: You must ensure "stream: true" in serialized `request`
57-
#[cfg(not(target_family = "wasm"))]
5856
#[crate::byot(
5957
T0 = serde::Serialize,
6058
R = serde::de::DeserializeOwned,
6159
stream = "true",
62-
where_clause = "R: std::marker::Send + 'static + TryFrom<eventsource_stream::Event, Error = OpenAIError>"
60+
where_clause = "R: crate::traits::MaybeSend + 'static + TryFrom<eventsource_stream::Event, Error = OpenAIError>"
6361
)]
6462
#[allow(unused_mut)]
6563
pub async fn create_and_run_stream(

async-openai/src/audio/speech.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use crate::{
55
Client, RequestOptions,
66
};
77

8-
#[cfg(not(target_family = "wasm"))]
98
use crate::types::audio::SpeechResponseStream;
109

1110
pub struct Speech<'c, C: Config> {
@@ -35,12 +34,11 @@ impl<'c, C: Config> Speech<'c, C> {
3534
}
3635

3736
/// Generates audio from the input text in SSE stream format.
38-
#[cfg(not(target_family = "wasm"))]
3937
#[crate::byot(
4038
T0 = serde::Serialize,
4139
R = serde::de::DeserializeOwned,
4240
stream = "true",
43-
where_clause = "R: std::marker::Send + 'static"
41+
where_clause = "R: crate::traits::MaybeSend + 'static"
4442
)]
4543
#[allow(unused_mut)]
4644
pub async fn create_stream(

async-openai/src/audio/transcriptions.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use crate::{
1010
Client, RequestOptions,
1111
};
1212

13-
#[cfg(not(target_family = "wasm"))]
1413
use crate::types::audio::TranscriptionResponseStream;
1514

1615
pub struct Transcriptions<'c, C: Config> {
@@ -30,7 +29,7 @@ impl<'c, C: Config> Transcriptions<'c, C> {
3029
#[crate::byot(
3130
T0 = Clone,
3231
R = serde::de::DeserializeOwned,
33-
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>",
32+
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>, T0: crate::traits::MaybeSend + 'static",
3433
)]
3534
pub async fn create(
3635
&self,
@@ -41,12 +40,11 @@ impl<'c, C: Config> Transcriptions<'c, C> {
4140
.await
4241
}
4342

44-
#[cfg(not(target_family = "wasm"))]
4543
#[crate::byot(
4644
T0 = Clone,
4745
R = serde::de::DeserializeOwned,
4846
stream = "true",
49-
where_clause = "R: std::marker::Send + 'static, reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>"
47+
where_clause = "R: crate::traits::MaybeSend + 'static, reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>, T0: crate::traits::MaybeSend + 'static"
5048
)]
5149
#[allow(unused_mut)]
5250
pub async fn create_stream(
@@ -74,7 +72,7 @@ impl<'c, C: Config> Transcriptions<'c, C> {
7472
#[crate::byot(
7573
T0 = Clone,
7674
R = serde::de::DeserializeOwned,
77-
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>",
75+
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>, T0: crate::traits::MaybeSend + 'static",
7876
)]
7977
pub async fn create_verbose_json(
8078
&self,
@@ -89,7 +87,7 @@ impl<'c, C: Config> Transcriptions<'c, C> {
8987
#[crate::byot(
9088
T0 = Clone,
9189
R = serde::de::DeserializeOwned,
92-
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>",
90+
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>, T0: crate::traits::MaybeSend + 'static",
9391
)]
9492
pub async fn create_diarized_json(
9593
&self,

async-openai/src/audio/translations.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ impl<'c, C: Config> Translations<'c, C> {
2727
#[crate::byot(
2828
T0 = Clone,
2929
R = serde::de::DeserializeOwned,
30-
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>",
30+
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>, T0: crate::traits::MaybeSend + 'static",
3131
)]
3232
pub async fn create(
3333
&self,
@@ -42,7 +42,7 @@ impl<'c, C: Config> Translations<'c, C> {
4242
#[crate::byot(
4343
T0 = Clone,
4444
R = serde::de::DeserializeOwned,
45-
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>",
45+
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>, T0: crate::traits::MaybeSend + 'static",
4646
)]
4747
pub async fn create_verbose_json(
4848
&self,

0 commit comments

Comments
 (0)