From f5ddd93f9095061753fd17ed8762952f29d18ba9 Mon Sep 17 00:00:00 2001 From: Albert Mavashev Date: Sat, 16 May 2026 11:32:21 -0400 Subject: [PATCH 1/3] examples: add async_openai_completion + point README at it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing /examples set (basic_usage, error_handling, guard_usage, streaming_usage, with_cycles_usage) all use an inline `call_llm` stub. That demonstrates the runcycles API surface cleanly, but it leaves evaluators without a working starting point for a real LLM call — they have to invent the async-openai wiring themselves, including token extraction from response.usage and cap-to-max_tokens application. This was flagged as the most likely cause of the 13:1 clone-to-install ratio on cycles-client-rust: lots of evaluators clone, look at the examples, and bounce because nothing shows the real-LLM composition. This PR fills that gap: - Adds `examples/async_openai_completion.rs` — a 60-line `with_cycles` example that wires async-openai 0.30.x against runcycles. Reserves `Amount::tokens(1_500)`, applies `caps.max_tokens` from ALLOW_WITH_CAPS, fires the chat completion, extracts `response.usage.total_tokens`, commits the actual. - Adds `async-openai = "0.30"` to dev-dependencies (dev-only — does not affect downstream consumers, but pulls into CI builds). - Updates the README's Quick Start section with a callout pointing at the new example for users who want a real LLM call rather than the `call_llm` placeholder. Verified locally: cargo check --example async_openai_completion --all-features ✓ cargo build --example async_openai_completion --all-features ✓ cargo clippy --example async_openai_completion --all-features -- -D warnings ✓ cargo fmt --check ✓ Companion docs PR in cycles-docs (PR #659) covers the same composition in detail (streaming via ReservationGuard, error-aware patterns, token-to-microcents conversion, gotchas). Out of scope (not in this PR): - `examples/axum_middleware.rs` — would close the parallel gap for the Rust web-framework audience. Axum middleware needs the tower::Service trait surface, which is enough complexity that it warrants its own PR with its own iteration cycle. - A streaming `async_openai_streaming.rs` example — the streaming pattern uses ReservationGuard (not with_cycles, since token totals arrive only after the stream ends). Worth a separate example for the same reason. --- Cargo.lock | 324 ++++++++++++++++++++++++++-- Cargo.toml | 1 + README.md | 2 + examples/async_openai_completion.rs | 89 ++++++++ 4 files changed, 401 insertions(+), 15 deletions(-) create mode 100644 examples/async_openai_completion.rs diff --git a/Cargo.lock b/Cargo.lock index f8161e5..d178e63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,43 @@ dependencies = [ "serde_json", ] +[[package]] +name = "async-openai" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bf39a15c8d613eb61892dc9a287c02277639ebead41ee611ad23aaa613f1a82" +dependencies = [ + "async-openai-macros", + "backoff", + "base64", + "bytes", + "derive_builder", + "eventsource-stream", + "futures", + "rand 0.9.4", + "reqwest", + "reqwest-eventsource", + "secrecy", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + +[[package]] +name = "async-openai-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81872a8e595e8ceceab71c6ba1f9078e313b452a1e31934e6763ef5d308705e4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -39,6 +76,20 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "futures-core", + "getrandom 0.2.17", + "instant", + "pin-project-lite", + "rand 0.8.6", + "tokio", +] + [[package]] name = "base64" version = "0.22.1" @@ -82,7 +133,7 @@ version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" dependencies = [ - "darling", + "darling 0.23.0", "ident_case", "prettyplease", "proc-macro2", @@ -151,14 +202,38 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", ] [[package]] @@ -174,13 +249,24 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn", +] + [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core", + "darling_core 0.23.0", "quote", "syn", ] @@ -203,6 +289,37 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + [[package]] name = "diff" version = "0.1.13" @@ -245,6 +362,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "eventsource-stream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +dependencies = [ + "futures-core", + "nom", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -364,6 +492,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.32" @@ -545,6 +679,7 @@ dependencies = [ "hyper", "hyper-util", "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", @@ -719,6 +854,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -816,6 +960,22 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" version = "1.2.0" @@ -844,6 +1004,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1016,8 +1186,8 @@ dependencies = [ "bit-vec", "bitflags", "num-traits", - "rand", - "rand_chacha", + "rand 0.9.4", + "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax", "rusty-fork", @@ -1045,7 +1215,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -1060,13 +1230,13 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand", + "rand 0.9.4", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -1107,14 +1277,35 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha", - "rand_core", + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] @@ -1124,7 +1315,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -1142,7 +1342,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core", + "rand_core 0.9.5", ] [[package]] @@ -1193,6 +1393,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-core", + "futures-util", "h2", "http", "http-body", @@ -1204,11 +1405,13 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "percent-encoding", "pin-project-lite", "quinn", "rustls", + "rustls-native-certs", "rustls-pki-types", "serde", "serde_json", @@ -1217,16 +1420,34 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots", ] +[[package]] +name = "reqwest-eventsource" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632c55746dbb44275691640e7b40c907c16a2dc1a5842aa98aaec90da6ec6bde" +dependencies = [ + "eventsource-stream", + "futures-core", + "futures-timer", + "mime", + "nom", + "pin-project-lite", + "reqwest", + "thiserror 1.0.69", +] + [[package]] name = "ring" version = "0.17.14" @@ -1245,13 +1466,14 @@ dependencies = [ name = "runcycles" version = "0.2.4" dependencies = [ + "async-openai", "bon", "pretty_assertions", "proptest", "reqwest", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", "tokio", "tokio-util", "tracing", @@ -1292,6 +1514,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -1352,6 +1586,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "serde", + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -1557,13 +1801,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1650,6 +1914,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -1751,6 +2026,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -1923,6 +2204,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" diff --git a/Cargo.toml b/Cargo.toml index dbc5e27..a183cee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ tracing = "0.1" uuid = { version = "1", features = ["v4"] } [dev-dependencies] +async-openai = "0.30" pretty_assertions = "1" proptest = "1" tokio = { version = "1", features = ["full", "test-util"] } diff --git a/README.md b/README.md index a187263..372e4a3 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ async fn main() -> Result<(), runcycles::Error> { # async fn call_llm(_: &str) -> String { "hi".into() } ``` +> **Want a real LLM example?** [`examples/async_openai_completion.rs`](examples/async_openai_completion.rs) wires the same `with_cycles` flow against `async-openai`, threading the response's `usage.total_tokens` back into the commit so the budget reflects actual spend. Run it with `cargo run --example async_openai_completion` (requires `OPENAI_API_KEY`). + ## Manual Control — RAII Guard For streaming, multi-step workflows, or when you need full control: diff --git a/examples/async_openai_completion.rs b/examples/async_openai_completion.rs new file mode 100644 index 0000000..63d667a --- /dev/null +++ b/examples/async_openai_completion.rs @@ -0,0 +1,89 @@ +//! Wrap a real OpenAI chat completion call with Cycles reserve-commit. +//! +//! This is the example most users actually want: instead of the `call_llm` +//! placeholder in `with_cycles_usage.rs`, this drives an actual `async-openai` +//! ChatCompletion request and feeds the response's `usage.total_tokens` back +//! into the Cycles commit so the budget reflects real spend. +//! +//! Requirements to run: +//! +//! - A running Cycles server reachable at the URL passed to +//! `CyclesClient::builder` (or override via `CYCLES_BASE_URL` env var). +//! - `OPENAI_API_KEY` set in the environment (async-openai's `Client::new` +//! reads it from there). +//! +//! Run: +//! +//! cargo run --example async_openai_completion +//! +//! For streaming chat completions, the lifecycle uses `ReservationGuard` +//! instead of `with_cycles` because token totals are only known after the +//! stream ends. See the docs site for the streaming pattern. + +use async_openai::{ + types::{ChatCompletionRequestUserMessageArgs, CreateChatCompletionRequestArgs}, + Client, +}; +use runcycles::models::*; +use runcycles::{with_cycles, CyclesClient, WithCyclesConfig}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let cycles = CyclesClient::builder("my-api-key", "http://localhost:7878") + .tenant("acme") + .build(); + + // Reads OPENAI_API_KEY from the environment. + let openai = Client::new(); + + let prompt = "Summarize the runcycles crate in one sentence."; + + let reply = with_cycles( + &cycles, + WithCyclesConfig::new(Amount::tokens(1_500)) + .action("llm.completion", "gpt-4o-mini") + .subject(Subject { + tenant: Some("acme".into()), + ..Default::default() + }), + |ctx| async move { + // If the server returned ALLOW_WITH_CAPS, narrow max_tokens. + let mut max_tokens: u32 = 800; + if let Some(caps) = &ctx.caps { + if let Some(cap) = caps.max_tokens { + // caps.max_tokens is i64; clamp into the OpenAI u32 range. + let cap_u32 = u32::try_from(cap.max(0)).unwrap_or(max_tokens); + max_tokens = cap_u32.min(max_tokens); + } + } + + let request = CreateChatCompletionRequestArgs::default() + .model("gpt-4o-mini") + .max_tokens(max_tokens) + .messages([ChatCompletionRequestUserMessageArgs::default() + .content(prompt) + .build()? + .into()]) + .build()?; + + let response = openai.chat().create(request).await?; + + let text = response + .choices + .first() + .and_then(|c| c.message.content.clone()) + .unwrap_or_default(); + + // The unit passed to with_cycles is TOKENS, so the actual must be + // tokens too. If usage is absent (some OpenAI-compatible providers + // omit it), treat as zero rather than over-charging. + let actual_tokens = response.usage.map(|u| u.total_tokens as i64).unwrap_or(0); + + Ok((text, Amount::tokens(actual_tokens))) + }, + ) + .await?; + + println!("Reply: {reply}"); + Ok(()) +} From 451a673c342bfbd19def0e94d36253e875389281 Mon Sep 17 00:00:00 2001 From: Albert Mavashev Date: Sat, 16 May 2026 11:42:57 -0400 Subject: [PATCH 2/3] examples: apply codex code review to async_openai_completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex flagged design/idiom issues that cargo check / clippy / fmt can't see — exactly the value of layered review on top of compile checks. All compile-pass, all human-readable now. Apply/skip tally: 8 applied, 0 pushed back. Applied: - **Silent under-billing fixed.** The original `response.usage.map(...).unwrap_or(0)` would commit `Amount::tokens(0)` when a provider omitted `usage` — silent under-billing on a successful-looking response, which is exactly wrong for a teaching example about budget governance. Now `response.usage.ok_or(...)?` so the closure errors and `with_cycles` auto-releases the reservation. Production code that needs a fallback must opt into one explicitly. - **Empty/missing content now errors.** `response.choices.first().and_then(|c| c.message.content.clone()).unwrap_or_default()` silently returned an empty string. Now uses `.ok_or(...)?` so a response with no choices or no content fails loud and the reservation releases. - **Zero/negative cap now errors.** `cap.max(0)` would have sent `max_tokens=0` to OpenAI (which OpenAI rejects, after we've already spent the request budget). Now `u32::try_from(cap)` errors on negatives, and a zero cap errors explicitly. Both before any network call to OpenAI. - **`.max_tokens()` → `.max_completion_tokens()`.** OpenAI deprecated `max_tokens` for chat completions in favor of `max_completion_tokens`. async-openai 0.30.1 supports both; the example uses the current name. - **`u.total_tokens as i64` → `i64::from(u.total_tokens)`.** More idiomatic, no `as` cast. - **Module doc comment accuracy:** - Removed the inaccurate "CYCLES_BASE_URL env var override" claim — the code hardcodes the URL. - Added a "Loud-failure stance" section explicitly explaining the new error-on-edge-cases design. - Softened the absolute "streaming uses ReservationGuard instead of with_cycles" framing — the guard is the right primitive when chunks are forwarded while the reservation remains open. - Removed editorial "the example most users actually want" tone. - **README callout under-stated requirements.** Now explicitly mentions: reachable Cycles server, tenant API key, and a TOKENS- denominated budget at the scope being reserved against — not just `OPENAI_API_KEY`. - **README claim softened.** Dropped "so the budget reflects actual spend" framing (which was directionally true only when usage is present) to "threading the response's usage.total_tokens back into the commit" — accurate regardless of failure path. Verified locally: cargo check --example async_openai_completion --all-features ✓ cargo clippy --example async_openai_completion --all-features -- -D warnings ✓ cargo fmt --check ✓ --- README.md | 2 +- examples/async_openai_completion.rs | 70 +++++++++++++++++++++-------- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 372e4a3..9fdb9d5 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ async fn main() -> Result<(), runcycles::Error> { # async fn call_llm(_: &str) -> String { "hi".into() } ``` -> **Want a real LLM example?** [`examples/async_openai_completion.rs`](examples/async_openai_completion.rs) wires the same `with_cycles` flow against `async-openai`, threading the response's `usage.total_tokens` back into the commit so the budget reflects actual spend. Run it with `cargo run --example async_openai_completion` (requires `OPENAI_API_KEY`). +> **Want a real LLM example?** [`examples/async_openai_completion.rs`](examples/async_openai_completion.rs) wires the same `with_cycles` flow against `async-openai`, threading the response's `usage.total_tokens` back into the commit. Run it with `cargo run --example async_openai_completion` — requires `OPENAI_API_KEY` in the env plus a reachable Cycles server, a tenant API key, and a `TOKENS`-denominated budget at the scope the example reserves against. ## Manual Control — RAII Guard diff --git a/examples/async_openai_completion.rs b/examples/async_openai_completion.rs index 63d667a..e348b73 100644 --- a/examples/async_openai_completion.rs +++ b/examples/async_openai_completion.rs @@ -1,24 +1,39 @@ //! Wrap a real OpenAI chat completion call with Cycles reserve-commit. //! -//! This is the example most users actually want: instead of the `call_llm` -//! placeholder in `with_cycles_usage.rs`, this drives an actual `async-openai` -//! ChatCompletion request and feeds the response's `usage.total_tokens` back -//! into the Cycles commit so the budget reflects real spend. +//! Instead of the `call_llm` placeholder in `with_cycles_usage.rs`, this drives +//! an actual `async-openai` ChatCompletion request and feeds the response's +//! `usage.total_tokens` back into the Cycles commit so the budget reflects +//! real spend. //! -//! Requirements to run: +//! ## Loud-failure stance //! -//! - A running Cycles server reachable at the URL passed to -//! `CyclesClient::builder` (or override via `CYCLES_BASE_URL` env var). +//! This example errors out instead of swallowing edge cases that would lead to +//! silent under-billing — a missing `usage` field, an empty `choices` array, +//! or a `caps.max_tokens` value of 0 all return `Err` from the closure, which +//! causes `with_cycles` to release the reservation rather than commit a wrong +//! amount. Production code that needs a fallback (e.g. commit the reservation +//! estimate when the provider omits `usage`) should make that choice +//! explicitly; the default should not be "commit zero." +//! +//! ## Requirements to run +//! +//! - A running Cycles server at the URL passed to `CyclesClient::builder` +//! (the example hardcodes `http://localhost:7878`), with a tenant `acme` +//! and a `TOKENS`-denominated budget that covers the reservation. +//! - A Cycles API key matching that tenant (the example hardcodes +//! `"my-api-key"`). //! - `OPENAI_API_KEY` set in the environment (async-openai's `Client::new` //! reads it from there). //! -//! Run: +//! ## Run //! //! cargo run --example async_openai_completion //! -//! For streaming chat completions, the lifecycle uses `ReservationGuard` -//! instead of `with_cycles` because token totals are only known after the -//! stream ends. See the docs site for the streaming pattern. +//! For streaming chat completions, the right primitive is `ReservationGuard` +//! rather than `with_cycles`, because the closure has to return both the +//! value and the actual cost in one shot — and a streamed response's total +//! token count is only known after the stream ends. See `streaming_usage.rs` +//! for the guard-based pattern. use async_openai::{ types::{ChatCompletionRequestUserMessageArgs, CreateChatCompletionRequestArgs}, @@ -47,19 +62,27 @@ async fn main() -> Result<(), Box> { ..Default::default() }), |ctx| async move { - // If the server returned ALLOW_WITH_CAPS, narrow max_tokens. + // Narrow max_tokens against the server's cap, if one was returned. + // A non-positive cap is a hard error — sending max_tokens=0 to + // OpenAI would request zero output, which is never what the caller + // wanted, and it would still consume the request budget. let mut max_tokens: u32 = 800; if let Some(caps) = &ctx.caps { if let Some(cap) = caps.max_tokens { - // caps.max_tokens is i64; clamp into the OpenAI u32 range. - let cap_u32 = u32::try_from(cap.max(0)).unwrap_or(max_tokens); + let cap_u32 = u32::try_from(cap) + .map_err(|_| "caps.max_tokens is negative — refusing to call OpenAI")?; + if cap_u32 == 0 { + return Err("caps.max_tokens is 0 — refusing to call OpenAI".into()); + } max_tokens = cap_u32.min(max_tokens); } } let request = CreateChatCompletionRequestArgs::default() .model("gpt-4o-mini") - .max_tokens(max_tokens) + // max_completion_tokens is the current field name; `max_tokens` + // is deprecated upstream for chat completions. + .max_completion_tokens(max_tokens) .messages([ChatCompletionRequestUserMessageArgs::default() .content(prompt) .build()? @@ -68,16 +91,25 @@ async fn main() -> Result<(), Box> { let response = openai.chat().create(request).await?; + // Choices empty / content missing → fail the closure so the + // reservation auto-releases instead of committing zero against + // a successful-looking response. let text = response .choices .first() .and_then(|c| c.message.content.clone()) - .unwrap_or_default(); + .ok_or("OpenAI response had no message content")?; // The unit passed to with_cycles is TOKENS, so the actual must be - // tokens too. If usage is absent (some OpenAI-compatible providers - // omit it), treat as zero rather than over-charging. - let actual_tokens = response.usage.map(|u| u.total_tokens as i64).unwrap_or(0); + // tokens too. We require `usage` to be present — committing zero + // on a missing-usage response silently under-bills the budget. + // OpenAI-compatible providers that omit usage (some local/proxy + // setups) should be wrapped with an explicit fallback in calling + // code; the example refuses to guess. + let usage = response + .usage + .ok_or("OpenAI response omitted usage — refusing to commit a guessed amount")?; + let actual_tokens = i64::from(usage.total_tokens); Ok((text, Amount::tokens(actual_tokens))) }, From 34b7703065832cf5187f377cf645f06ca5d8312b Mon Sep 17 00:00:00 2001 From: Albert Mavashev Date: Sat, 16 May 2026 11:53:00 -0400 Subject: [PATCH 3/3] =?UTF-8?q?fix(ci):=20bump=20async-openai=200.30=20?= =?UTF-8?q?=E2=86=92=200.38=20to=20clear=20cargo=20audit=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's `cargo audit --deny warnings` step failed on this PR because the 0.30.x line of async-openai pulled in two unmaintained transitive deps that the org-wide audit job treats as errors: - backoff 0.4.0 (RUSTSEC-2025-0012, unmaintained) - instant 0.1.13 (RUSTSEC-2024-0384, unmaintained, via backoff) async-openai 0.31+ replaced its retry stack with `tower`, dropping the backoff dependency entirely. Bumping to 0.38 (current latest) clears both advisories without needing audit-ignore configuration. API changes between 0.30 and 0.38 are minor for the example's usage: - `Client` is now gated behind the per-API features (the crate split its surface in 0.31). Enabled `chat-completion` for the example. - Chat-completion types moved from `async_openai::types::` to `async_openai::types::chat::`. Updated the import. - `default-features = false, features = ["chat-completion", "rustls"]` keeps the dev-dep set minimal — no unused-feature surface, and rustls matches the rest of the runcycles crate's TLS story. The example still uses the same `with_cycles` flow, the same `response.usage.total_tokens` extraction, and the same loud-failure patterns from the codex-round-1 fixes. Only the imports and Cargo.toml changed. Verified locally: cargo check --example async_openai_completion --all-features ✓ cargo clippy --example async_openai_completion --all-features -- -D warnings ✓ cargo fmt --check ✓ cargo tree -i backoff → "did not match any packages" ✓ cargo tree -i instant → "did not match any packages" ✓ Note: cycles-docs PR #659 currently pins async-openai 0.30.x in its how-to doc; that doc needs a parallel bump to 0.38 + types::chat:: in a separate commit on that PR. --- Cargo.lock | 375 +++++++++++++++++++--------- Cargo.toml | 2 +- examples/async_openai_completion.rs | 2 +- 3 files changed, 265 insertions(+), 114 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d178e63..b3996a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,35 +29,37 @@ dependencies = [ [[package]] name = "async-openai" -version = "0.30.1" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf39a15c8d613eb61892dc9a287c02277639ebead41ee611ad23aaa613f1a82" +checksum = "2bb666d696156a8da537278ecb3195806d885edf25a573ce23ceec4dee2b0eee" dependencies = [ "async-openai-macros", - "backoff", "base64", "bytes", "derive_builder", "eventsource-stream", "futures", - "rand 0.9.4", - "reqwest", - "reqwest-eventsource", + "getrandom 0.3.4", + "rand", + "reqwest 0.13.3", "secrecy", "serde", "serde_json", - "thiserror 2.0.18", + "serde_urlencoded", + "thiserror", "tokio", "tokio-stream", "tokio-util", + "tower", "tracing", + "url", ] [[package]] name = "async-openai-macros" -version = "0.1.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81872a8e595e8ceceab71c6ba1f9078e313b452a1e31934e6763ef5d308705e4" +checksum = "492a944774207eed3acf425214eadbd6ce84a2b89331164ff1c11bae92b26302" dependencies = [ "proc-macro2", "quote", @@ -77,17 +79,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "backoff" -version = "0.4.0" +name = "aws-lc-rs" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" dependencies = [ - "futures-core", - "getrandom 0.2.17", - "instant", - "pin-project-lite", - "rand 0.8.6", - "tokio", + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", ] [[package]] @@ -161,6 +171,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -176,6 +188,25 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -337,6 +368,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -421,6 +458,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.32" @@ -492,12 +535,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - [[package]] name = "futures-util" version = "0.3.32" @@ -679,7 +716,6 @@ dependencies = [ "hyper", "hyper-util", "rustls", - "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", @@ -854,15 +890,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "ipnet" version = "2.12.0" @@ -885,6 +912,65 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.93" @@ -1186,8 +1272,8 @@ dependencies = [ "bit-vec", "bitflags", "num-traits", - "rand 0.9.4", - "rand_chacha 0.9.0", + "rand", + "rand_chacha", "rand_xorshift", "regex-syntax", "rusty-fork", @@ -1215,7 +1301,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "web-time", @@ -1227,16 +1313,17 @@ version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.4", + "rand", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror", "tinyvec", "tracing", "web-time", @@ -1277,35 +1364,14 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "rand_chacha", + "rand_core", ] [[package]] @@ -1315,16 +1381,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", + "rand_core", ] [[package]] @@ -1342,7 +1399,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core 0.9.5", + "rand_core", ] [[package]] @@ -1393,7 +1450,6 @@ dependencies = [ "bytes", "encoding_rs", "futures-core", - "futures-util", "h2", "http", "http-body", @@ -1405,13 +1461,11 @@ dependencies = [ "js-sys", "log", "mime", - "mime_guess", "native-tls", "percent-encoding", "pin-project-lite", "quinn", "rustls", - "rustls-native-certs", "rustls-pki-types", "serde", "serde_json", @@ -1420,32 +1474,56 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls", - "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", "web-sys", "webpki-roots", ] [[package]] -name = "reqwest-eventsource" -version = "0.6.0" +name = "reqwest" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632c55746dbb44275691640e7b40c907c16a2dc1a5842aa98aaec90da6ec6bde" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ - "eventsource-stream", + "base64", + "bytes", "futures-core", - "futures-timer", - "mime", - "nom", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime_guess", + "percent-encoding", "pin-project-lite", - "reqwest", - "thiserror 1.0.69", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", ] [[package]] @@ -1470,10 +1548,10 @@ dependencies = [ "bon", "pretty_assertions", "proptest", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tracing", @@ -1487,6 +1565,15 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.4" @@ -1506,6 +1593,7 @@ version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ + "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", @@ -1536,12 +1624,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -1571,6 +1687,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.29" @@ -1696,6 +1821,22 @@ dependencies = [ "libc", ] +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" @@ -1801,33 +1942,13 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -1949,8 +2070,10 @@ dependencies = [ "pin-project-lite", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -2094,6 +2217,16 @@ dependencies = [ "libc", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -2206,9 +2339,9 @@ dependencies = [ [[package]] name = "wasm-streams" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ "futures-util", "js-sys", @@ -2249,6 +2382,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "1.0.6" @@ -2258,6 +2400,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-link" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index a183cee..57f175e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ tracing = "0.1" uuid = { version = "1", features = ["v4"] } [dev-dependencies] -async-openai = "0.30" +async-openai = { version = "0.38", default-features = false, features = ["chat-completion", "rustls"] } pretty_assertions = "1" proptest = "1" tokio = { version = "1", features = ["full", "test-util"] } diff --git a/examples/async_openai_completion.rs b/examples/async_openai_completion.rs index e348b73..385b1ec 100644 --- a/examples/async_openai_completion.rs +++ b/examples/async_openai_completion.rs @@ -36,7 +36,7 @@ //! for the guard-based pattern. use async_openai::{ - types::{ChatCompletionRequestUserMessageArgs, CreateChatCompletionRequestArgs}, + types::chat::{ChatCompletionRequestUserMessageArgs, CreateChatCompletionRequestArgs}, Client, }; use runcycles::models::*;