Skip to content
Merged
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
5 changes: 1 addition & 4 deletions async-openai-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "async-openai-macros"
version = "0.2.0"
version = "0.3.0"
authors = ["Himanshu Neema"]
keywords = ["openai", "macros", "ai"]
description = "Macros for async-openai"
Expand All @@ -14,9 +14,6 @@ readme = "README.md"
[lib]
proc-macro = true

[features]
middleware = []

[dependencies]
syn = { version = "2.0", features = ["full"] }
quote = "1.0"
Expand Down
17 changes: 2 additions & 15 deletions async-openai-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{
parse::{Parse, ParseStream},
parse_macro_input, parse_quote,
parse_macro_input,
punctuated::Punctuated,
token::Comma,
FnArg, GenericParam, Generics, ItemFn, Pat, PatType, TypeParam, WhereClause,
Expand Down Expand Up @@ -57,7 +57,6 @@ pub fn byot(args: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let mut new_generics = Generics::default();
let mut param_count = 0;
let middleware_enabled = cfg!(feature = "middleware");

// Process function arguments
let mut new_params = Vec::new();
Expand All @@ -83,18 +82,6 @@ pub fn byot(args: TokenStream, item: TokenStream) -> TokenStream {
{
type_param.bounds.extend(vec![bound.clone()]);
}
let needs_middleware_replay_bounds =
bounds_args.bounds.iter().any(|(name, bound)| {
name == &generic_name
&& bound.to_token_stream().to_string().contains("Clone")
&& !bound.to_token_stream().to_string().contains("Display")
});
if middleware_enabled && needs_middleware_replay_bounds {
type_param
.bounds
.push(parse_quote!(crate::middleware::MiddlewareInput));
}

new_params.push(GenericParam::Type(type_param));
param_count += 1;
quote! { #pat_ident: #generic_ident }
Expand Down Expand Up @@ -136,7 +123,7 @@ pub fn byot(args: TokenStream, item: TokenStream) -> TokenStream {

// Generate return type based on stream flag
let return_type = if bounds_args.stream {
quote! { Result<::std::pin::Pin<Box<dyn ::futures::Stream<Item = Result<R, OpenAIError>> + Send>>, OpenAIError> }
quote! { Result<crate::types::stream::StreamResponse<R>, OpenAIError> }
} else {
quote! { Result<R, OpenAIError> }
};
Expand Down
10 changes: 5 additions & 5 deletions async-openai/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ chat-completion = ["chat-completion-types", "_api"]
assistant = ["assistant-types", "_api", ]
administration = ["administration-types", "_api"]
completions = ["completion-types", "_api"]
middleware = ["dep:tower", "_api", "async-openai-macros?/middleware"]
middleware = ["dep:tower", "_api"]

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

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

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

Expand Down
17 changes: 6 additions & 11 deletions async-openai/MIDDLEWARE.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,22 +91,17 @@ Custom tower retry policies can call `middleware::retry::should_retry` to reuse

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

## Native and WASM bounds

The conceptual middleware boundary stays the same; only the platform thread-safety bounds differ.
## Error Handling

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

On WASM targets, middleware services and futures must be `'static`.
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.

## Bring Your Own Types Interaction
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.

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.

## Error Handling
## Bring Your Own Types Interaction

`OpenAIError::Boxed` is available only when the `middleware` feature is enabled.
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.

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.

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.
42 changes: 22 additions & 20 deletions async-openai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@

## Overview

`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:
`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.

<details>
<summary>Feature Flags</summary>

| What | APIs | Crate Feature Flags |
|---|---|---|
Expand All @@ -36,16 +39,18 @@
| **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` |
| **Legacy** | Completions | `completions` |

Features that makes `async-openai` unique:
</details>


- Bring your own custom types for Request or Response objects.
- SSE streaming on available APIs.
- Customize path, query and headers per request; customize path and headers globally (for all requests).
- Requests (except SSE streaming) including form submissions are retried with exponential backoff when [rate limited](https://platform.openai.com/docs/guides/rate-limits).
- Requests are retried with exponential backoff when [rate limited](https://platform.openai.com/docs/guides/rate-limits).
- Ergonomic builder pattern for all request objects.
- Granular feature flags to enable any types or apis: good for faster compilation and crate reuse.
- Microsoft Azure OpenAI Service (only for APIs matching OpenAI spec).
- WASM (doesn't support streaming yet)
- Middleware support with [tower](https://crates.io/crates/tower) ecosystem
- SSE streaming.
- Customize path, query and headers per request or for all requests.
- Granular feature flags to enable any types or apis.
- Microsoft Azure OpenAI Service.
- WASM.
- Middleware support with [tower](https://crates.io/crates/tower) ecosystem.

## Usage

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

## Webhooks

Support for webhook includes event types, signature verification, and building webhook events from payloads.

## Bring Your Own Types

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

`byot` requires trait bounds:
- a request type (`fn` input parameter) needs to implement `serde::Serialize` or `std::fmt::Display` trait
- a response type (`fn` ouput parameter) needs to implement `serde::de::DeserializeOwned` trait.

For example, to use `serde_json::Value` as request and response type:
```rust
let response: Value = client
Expand All @@ -147,9 +144,11 @@ let response: Value = client
This can be useful in many scenarios:
- To use this library with other OpenAI compatible APIs whose types don't exactly match OpenAI.
- Extend existing types in this crate with new fields with `serde` (for example with `#[serde(flatten)]`).
- To avoid verbose types.
- To avoid typing verbose types.
- To escape deserialization errors.

`*_byot` methods require same trait bounds as regular methods.

Visit [examples/bring-your-own-type](https://github.com/64bit/async-openai/tree/main/examples/bring-your-own-type)
directory to learn more.

Expand Down Expand Up @@ -217,8 +216,7 @@ client

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

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

For example:

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

## Webhooks

Support for webhook includes event types, signature verification, and building webhook events from payloads.

## Middleware

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.
Expand Down
7 changes: 2 additions & 5 deletions async-openai/src/assistants/runs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ use crate::{
Client, RequestOptions,
};

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

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

/// byot: You must ensure "stream: true" in serialized `request`
#[cfg(not(target_family = "wasm"))]
#[crate::byot(
T0 = std::fmt::Display,
T1 = serde::Serialize,
R = serde::de::DeserializeOwned,
stream = "true",
where_clause = "R: std::marker::Send + 'static + TryFrom<eventsource_stream::Event, Error = OpenAIError>"
where_clause = "R: crate::traits::MaybeSend + 'static + TryFrom<eventsource_stream::Event, Error = OpenAIError>"
)]
#[allow(unused_mut)]
pub async fn submit_tool_outputs_stream(
Expand Down
4 changes: 1 addition & 3 deletions async-openai/src/assistants/threads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use crate::{
Client, Messages, RequestOptions, Runs,
};

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

/// Create threads that assistants can interact with.
Expand Down Expand Up @@ -54,12 +53,11 @@ impl<'c, C: Config> Threads<'c, C> {
/// Create a thread and run it in one request (streaming).
///
/// byot: You must ensure "stream: true" in serialized `request`
#[cfg(not(target_family = "wasm"))]
#[crate::byot(
T0 = serde::Serialize,
R = serde::de::DeserializeOwned,
stream = "true",
where_clause = "R: std::marker::Send + 'static + TryFrom<eventsource_stream::Event, Error = OpenAIError>"
where_clause = "R: crate::traits::MaybeSend + 'static + TryFrom<eventsource_stream::Event, Error = OpenAIError>"
)]
#[allow(unused_mut)]
pub async fn create_and_run_stream(
Expand Down
4 changes: 1 addition & 3 deletions async-openai/src/audio/speech.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use crate::{
Client, RequestOptions,
};

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

pub struct Speech<'c, C: Config> {
Expand Down Expand Up @@ -35,12 +34,11 @@ impl<'c, C: Config> Speech<'c, C> {
}

/// Generates audio from the input text in SSE stream format.
#[cfg(not(target_family = "wasm"))]
#[crate::byot(
T0 = serde::Serialize,
R = serde::de::DeserializeOwned,
stream = "true",
where_clause = "R: std::marker::Send + 'static"
where_clause = "R: crate::traits::MaybeSend + 'static"
)]
#[allow(unused_mut)]
pub async fn create_stream(
Expand Down
10 changes: 4 additions & 6 deletions async-openai/src/audio/transcriptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use crate::{
Client, RequestOptions,
};

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

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

#[cfg(not(target_family = "wasm"))]
#[crate::byot(
T0 = Clone,
R = serde::de::DeserializeOwned,
stream = "true",
where_clause = "R: std::marker::Send + 'static, reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>"
where_clause = "R: crate::traits::MaybeSend + 'static, reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>, T0: crate::traits::MaybeSend + 'static"
)]
#[allow(unused_mut)]
pub async fn create_stream(
Expand Down Expand Up @@ -74,7 +72,7 @@ impl<'c, C: Config> Transcriptions<'c, C> {
#[crate::byot(
T0 = Clone,
R = serde::de::DeserializeOwned,
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>",
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>, T0: crate::traits::MaybeSend + 'static",
)]
pub async fn create_verbose_json(
&self,
Expand All @@ -89,7 +87,7 @@ impl<'c, C: Config> Transcriptions<'c, C> {
#[crate::byot(
T0 = Clone,
R = serde::de::DeserializeOwned,
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>",
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>, T0: crate::traits::MaybeSend + 'static",
)]
pub async fn create_diarized_json(
&self,
Expand Down
4 changes: 2 additions & 2 deletions async-openai/src/audio/translations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl<'c, C: Config> Translations<'c, C> {
#[crate::byot(
T0 = Clone,
R = serde::de::DeserializeOwned,
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>",
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>, T0: crate::traits::MaybeSend + 'static",
)]
pub async fn create(
&self,
Expand All @@ -42,7 +42,7 @@ impl<'c, C: Config> Translations<'c, C> {
#[crate::byot(
T0 = Clone,
R = serde::de::DeserializeOwned,
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>",
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>, T0: crate::traits::MaybeSend + 'static",
)]
pub async fn create_verbose_json(
&self,
Expand Down
2 changes: 1 addition & 1 deletion async-openai/src/audio/voice_consents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ impl<'c, C: Config> VoiceConsents<'c, C> {
#[crate::byot(
T0 = Clone,
R = serde::de::DeserializeOwned,
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>",
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>, T0: crate::traits::MaybeSend + 'static",
)]
pub async fn create(
&self,
Expand Down
2 changes: 1 addition & 1 deletion async-openai/src/audio/voices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ impl<'c, C: Config> Voices<'c, C> {
#[crate::byot(
T0 = Clone,
R = serde::de::DeserializeOwned,
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>",
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>, T0: crate::traits::MaybeSend + 'static",
)]
pub async fn create(&self, request: CreateVoiceRequest) -> Result<VoiceResource, OpenAIError> {
self.client
Expand Down
Loading
Loading