Skip to content

Commit 199b9a3

Browse files
chore: Bring async-openai into repo as request starter (#2520)
Co-authored-by: Graham King <[email protected]>
1 parent 26d9f15 commit 199b9a3

File tree

114 files changed

+15700
-345
lines changed

Some content is hidden

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

114 files changed

+15700
-345
lines changed

Cargo.lock

Lines changed: 193 additions & 42 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ members = [
99
"lib/llm",
1010
"lib/runtime",
1111
"lib/tokens",
12+
"lib/async-openai",
13+
"lib/async-openai-macros",
1214
"lib/bindings/c",
1315
"lib/engines/*",
1416
]
@@ -29,11 +31,11 @@ keywords = ["llm", "genai", "inference", "nvidia", "distributed", "dynamo"]
2931
dynamo-runtime = { path = "lib/runtime", version = "0.4.0" }
3032
dynamo-llm = { path = "lib/llm", version = "0.4.0" }
3133
dynamo-tokens = { path = "lib/tokens", version = "0.4.0" }
34+
dynamo-async-openai = { path = "lib/async-openai", version = "0.4.0", features = ["byot", "rustls"]}
3235

3336
# External dependencies
3437
anyhow = { version = "1" }
3538
async-nats = { version = "0.40", features = ["service"] }
36-
async-openai = { version = "0.29.0", features = ["rustls", "byot"] }
3739
async-stream = { version = "0.3" }
3840
async-trait = { version = "0.1" }
3941
async_zmq = { version = "0.4.0" }

launch/dynamo-run/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ dynamo-engine-llamacpp = { path = "../../lib/engines/llamacpp", optional = true
3333
dynamo-engine-mistralrs = { path = "../../lib/engines/mistralrs", optional = true }
3434

3535
anyhow = { workspace = true }
36-
async-openai = { workspace = true }
36+
dynamo-async-openai = { workspace = true }
3737
async-stream = { workspace = true }
3838
async-trait = { workspace = true }
3939
either = { workspace = true }

lib/async-openai-macros/Cargo.toml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Based on https://github.com/64bit/async-openai/ by Himanshu Neema
5+
# Original Copyright (c) 2022 Himanshu Neema
6+
# Licensed under MIT License (see ATTRIBUTIONS-Rust.md)
7+
#
8+
# Modifications Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES.
9+
# Licensed under Apache 2.0
10+
11+
[package]
12+
name = "async-openai-macros"
13+
version.workspace = true
14+
edition.workspace = true
15+
authors.workspace = true
16+
license.workspace = true
17+
homepage.workspace = true
18+
repository.workspace = true
19+
readme.workspace = true
20+
21+
[lib]
22+
proc-macro = true
23+
24+
[dependencies]
25+
syn = { version = "2.0", features = ["full"] }
26+
quote = "1.0"
27+
proc-macro2 = "1.0"

lib/async-openai-macros/src/lib.rs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
// Based on https://github.com/64bit/async-openai/ by Himanshu Neema
5+
// Original Copyright (c) 2022 Himanshu Neema
6+
// Licensed under MIT License (see ATTRIBUTIONS-Rust.md)
7+
//
8+
// Modifications Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES.
9+
// Licensed under Apache 2.0
10+
11+
use proc_macro::TokenStream;
12+
use quote::{quote, ToTokens};
13+
use syn::{
14+
parse::{Parse, ParseStream},
15+
parse_macro_input,
16+
punctuated::Punctuated,
17+
token::Comma,
18+
FnArg, GenericParam, Generics, ItemFn, Pat, PatType, TypeParam, WhereClause,
19+
};
20+
21+
// Parse attribute arguments like #[byot(T0: Display + Debug, T1: Clone, R: Serialize)]
22+
struct BoundArgs {
23+
bounds: Vec<(String, syn::TypeParamBound)>,
24+
where_clause: Option<String>,
25+
stream: bool, // Add stream flag
26+
}
27+
28+
impl Parse for BoundArgs {
29+
fn parse(input: ParseStream) -> syn::Result<Self> {
30+
let mut bounds = Vec::new();
31+
let mut where_clause = None;
32+
let mut stream = false; // Default to false
33+
let vars = Punctuated::<syn::MetaNameValue, Comma>::parse_terminated(input)?;
34+
35+
for var in vars {
36+
let name = var.path.get_ident().unwrap().to_string();
37+
match name.as_str() {
38+
"where_clause" => {
39+
where_clause = Some(var.value.into_token_stream().to_string());
40+
}
41+
"stream" => {
42+
stream = var.value.into_token_stream().to_string().contains("true");
43+
}
44+
_ => {
45+
let bound: syn::TypeParamBound =
46+
syn::parse_str(&var.value.into_token_stream().to_string())?;
47+
bounds.push((name, bound));
48+
}
49+
}
50+
}
51+
Ok(BoundArgs {
52+
bounds,
53+
where_clause,
54+
stream,
55+
})
56+
}
57+
}
58+
59+
#[proc_macro_attribute]
60+
pub fn byot_passthrough(_args: TokenStream, item: TokenStream) -> TokenStream {
61+
item
62+
}
63+
64+
#[proc_macro_attribute]
65+
pub fn byot(args: TokenStream, item: TokenStream) -> TokenStream {
66+
let bounds_args = parse_macro_input!(args as BoundArgs);
67+
let input = parse_macro_input!(item as ItemFn);
68+
let mut new_generics = Generics::default();
69+
let mut param_count = 0;
70+
71+
// Process function arguments
72+
let mut new_params = Vec::new();
73+
let args = input
74+
.sig
75+
.inputs
76+
.iter()
77+
.map(|arg| {
78+
match arg {
79+
FnArg::Receiver(receiver) => receiver.to_token_stream(),
80+
FnArg::Typed(PatType { pat, .. }) => {
81+
if let Pat::Ident(pat_ident) = &**pat {
82+
let generic_name = format!("T{}", param_count);
83+
let generic_ident =
84+
syn::Ident::new(&generic_name, proc_macro2::Span::call_site());
85+
86+
// Create type parameter with optional bounds
87+
let mut type_param = TypeParam::from(generic_ident.clone());
88+
if let Some((_, bound)) = bounds_args
89+
.bounds
90+
.iter()
91+
.find(|(name, _)| name == &generic_name)
92+
{
93+
type_param.bounds.extend(vec![bound.clone()]);
94+
}
95+
96+
new_params.push(GenericParam::Type(type_param));
97+
param_count += 1;
98+
quote! { #pat_ident: #generic_ident }
99+
} else {
100+
arg.to_token_stream()
101+
}
102+
}
103+
}
104+
})
105+
.collect::<Vec<_>>();
106+
107+
// Add R type parameter with optional bounds
108+
let generic_r = syn::Ident::new("R", proc_macro2::Span::call_site());
109+
let mut return_type_param = TypeParam::from(generic_r.clone());
110+
if let Some((_, bound)) = bounds_args.bounds.iter().find(|(name, _)| name == "R") {
111+
return_type_param.bounds.extend(vec![bound.clone()]);
112+
}
113+
new_params.push(GenericParam::Type(return_type_param));
114+
115+
// Add all generic parameters
116+
new_generics.params.extend(new_params);
117+
118+
let fn_name = &input.sig.ident;
119+
let byot_fn_name = syn::Ident::new(&format!("{}_byot", fn_name), fn_name.span());
120+
let vis = &input.vis;
121+
let block = &input.block;
122+
let attrs = &input.attrs;
123+
let asyncness = &input.sig.asyncness;
124+
125+
// Parse where clause if provided
126+
let where_clause = if let Some(where_str) = bounds_args.where_clause {
127+
match syn::parse_str::<WhereClause>(&format!("where {}", where_str.replace("\"", ""))) {
128+
Ok(where_clause) => quote! { #where_clause },
129+
Err(e) => return TokenStream::from(e.to_compile_error()),
130+
}
131+
} else {
132+
quote! {}
133+
};
134+
135+
// Generate return type based on stream flag
136+
let return_type = if bounds_args.stream {
137+
quote! { Result<::std::pin::Pin<Box<dyn ::futures::Stream<Item = Result<R, OpenAIError>> + Send>>, OpenAIError> }
138+
} else {
139+
quote! { Result<R, OpenAIError> }
140+
};
141+
142+
let expanded = quote! {
143+
#(#attrs)*
144+
#input
145+
146+
#(#attrs)*
147+
#vis #asyncness fn #byot_fn_name #new_generics (#(#args),*) -> #return_type #where_clause #block
148+
};
149+
150+
expanded.into()
151+
}

lib/async-openai/Cargo.toml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Based on https://github.com/64bit/async-openai/ by Himanshu Neema
5+
# Original Copyright (c) 2022 Himanshu Neema
6+
# Licensed under MIT License (see ATTRIBUTIONS-Rust.md)
7+
#
8+
# Modifications Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES.
9+
# Licensed under Apache 2.0
10+
11+
[package]
12+
name = "dynamo-async-openai"
13+
version.workspace = true
14+
edition.workspace = true
15+
authors.workspace = true
16+
license.workspace = true
17+
homepage.workspace = true
18+
repository.workspace = true
19+
readme.workspace = true
20+
21+
[features]
22+
default = ["rustls"]
23+
# Enable rustls for TLS support
24+
rustls = ["reqwest/rustls-tls-native-roots"]
25+
# Enable rustls and webpki-roots
26+
rustls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"]
27+
# Enable native-tls for TLS support
28+
native-tls = ["reqwest/native-tls"]
29+
# Remove dependency on OpenSSL
30+
native-tls-vendored = ["reqwest/native-tls-vendored"]
31+
realtime = ["dep:tokio-tungstenite"]
32+
# Bring your own types
33+
byot = []
34+
35+
[dependencies]
36+
async-openai-macros = { path = "../async-openai-macros" }
37+
backoff = { version = "0.4.0", features = ["tokio"] }
38+
base64 = "0.22.1"
39+
futures = "0.3.31"
40+
rand = "0.9.0"
41+
reqwest = { version = "0.12.12", features = [
42+
"json",
43+
"stream",
44+
"multipart",
45+
], default-features = false }
46+
reqwest-eventsource = "0.6.0"
47+
serde = { version = "1.0.217", features = ["derive", "rc"] }
48+
serde_json = "1.0.135"
49+
thiserror = "2.0.11"
50+
tokio = { version = "1.43.0", features = ["fs", "macros"] }
51+
tokio-stream = "0.1.17"
52+
tokio-util = { version = "0.7.13", features = ["codec", "io-util"] }
53+
tracing = "0.1.41"
54+
derive_builder = "0.20.2"
55+
secrecy = { version = "0.10.3", features = ["serde"] }
56+
bytes = "1.9.0"
57+
eventsource-stream = "0.2.3"
58+
tokio-tungstenite = { version = "0.26.1", optional = true, default-features = false }
59+
60+
[dev-dependencies]
61+
tokio-test = "0.4.4"
62+
serde_json = "1.0"
63+
64+
[[test]]
65+
name = "bring-your-own-type"
66+
required-features = ["byot"]
67+
68+
[package.metadata.docs.rs]
69+
all-features = true
70+
rustdoc-args = ["--cfg", "docsrs"]

lib/async-openai/src/assistants.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
// Based on https://github.com/64bit/async-openai/ by Himanshu Neema
5+
// Original Copyright (c) 2022 Himanshu Neema
6+
// Licensed under MIT License (see ATTRIBUTIONS-Rust.md)
7+
//
8+
// Modifications Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES.
9+
// Licensed under Apache 2.0
10+
11+
use serde::Serialize;
12+
13+
use crate::{
14+
config::Config,
15+
error::OpenAIError,
16+
types::{
17+
AssistantObject, CreateAssistantRequest, DeleteAssistantResponse, ListAssistantsResponse,
18+
ModifyAssistantRequest,
19+
},
20+
Client,
21+
};
22+
23+
/// Build assistants that can call models and use tools to perform tasks.
24+
///
25+
/// [Get started with the Assistants API](https://platform.openai.com/docs/assistants)
26+
pub struct Assistants<'c, C: Config> {
27+
client: &'c Client<C>,
28+
}
29+
30+
impl<'c, C: Config> Assistants<'c, C> {
31+
pub fn new(client: &'c Client<C>) -> Self {
32+
Self { client }
33+
}
34+
35+
/// Create an assistant with a model and instructions.
36+
#[crate::byot(T0 = serde::Serialize, R = serde::de::DeserializeOwned)]
37+
pub async fn create(
38+
&self,
39+
request: CreateAssistantRequest,
40+
) -> Result<AssistantObject, OpenAIError> {
41+
self.client.post("/assistants", request).await
42+
}
43+
44+
/// Retrieves an assistant.
45+
#[crate::byot(T0 = std::fmt::Display, R = serde::de::DeserializeOwned)]
46+
pub async fn retrieve(&self, assistant_id: &str) -> Result<AssistantObject, OpenAIError> {
47+
self.client
48+
.get(&format!("/assistants/{assistant_id}"))
49+
.await
50+
}
51+
52+
/// Modifies an assistant.
53+
#[crate::byot(T0 = std::fmt::Display, T1 = serde::Serialize, R = serde::de::DeserializeOwned)]
54+
pub async fn update(
55+
&self,
56+
assistant_id: &str,
57+
request: ModifyAssistantRequest,
58+
) -> Result<AssistantObject, OpenAIError> {
59+
self.client
60+
.post(&format!("/assistants/{assistant_id}"), request)
61+
.await
62+
}
63+
64+
/// Delete an assistant.
65+
#[crate::byot(T0 = std::fmt::Display, R = serde::de::DeserializeOwned)]
66+
pub async fn delete(&self, assistant_id: &str) -> Result<DeleteAssistantResponse, OpenAIError> {
67+
self.client
68+
.delete(&format!("/assistants/{assistant_id}"))
69+
.await
70+
}
71+
72+
/// Returns a list of assistants.
73+
#[crate::byot(T0 = serde::Serialize, R = serde::de::DeserializeOwned)]
74+
pub async fn list<Q>(&self, query: &Q) -> Result<ListAssistantsResponse, OpenAIError>
75+
where
76+
Q: Serialize + ?Sized,
77+
{
78+
self.client.get_with_query("/assistants", &query).await
79+
}
80+
}

0 commit comments

Comments
 (0)