Skip to content

docs(rust): add config reference + async-openai integration to close conversion gap#659

Merged
amavashev merged 3 commits into
mainfrom
docs/rust-client-config-and-llm-integration
May 16, 2026
Merged

docs(rust): add config reference + async-openai integration to close conversion gap#659
amavashev merged 3 commits into
mainfrom
docs/rust-client-config-and-llm-integration

Conversation

@amavashev
Copy link
Copy Markdown
Contributor

Why

Investigation of the cycles-client-rust 13:1 clone-to-install ratio (907 clones, 67 installs over 6 weeks). For comparison, the TypeScript client runs 1.17:1 and Python runs 0.37:1. Rust is an order of magnitude off — even allowing for Rust ecosystem culture (more source-inspection than npm/pip), 67 installs after 6 weeks is low for a crate with otherwise solid presentation.

The crate itself is in good shape: clean crates.io page with proper description and keywords, docs.rs renders at 99.77% coverage, the GitHub README has badges + cargo block + 3-import quickstart, and /examples has 5 files. No obvious surface defect.

The friction is in the docs surface in this repo. Two concrete gaps:

Gap 1: Missing Rust client configuration reference

Python, TypeScript, and Spring Boot all have a `configuration/*-client-configuration-reference.md` doc. Rust did not. An evaluator looking up retry tuning, custom `reqwest::Client` injection, the blocking variant, or env var prefix customization had nowhere to land. The sidebar's Configuration section literally listed three clients, not four.

Gap 2: No real LLM-integration example

The Rust quickstart and integration guide both use `call_llm()` / `openai_call()` / `stream_llm_response()` as placeholders. An evaluator who copied the sample code had to invent the async-openai wiring themselves — including streaming token capture (the `stream_options.include_usage` gotcha), OpenAI-vs-Cycles error mapping, and cap-to-max_tokens application.

This is the most plausible explanation for the conversion gap: a Rust developer who lands on the quickstart, sees `call_llm()`, and bounces without ever running `cargo add runcycles`.

What's in this PR

Two new docs:

  • `configuration/rust-client-configuration-reference.md` — full `CyclesConfig` field reference (verified against cycles-client-rust src/config.rs), env vars including the Rust-specific custom prefix support, builder API method-by-method, custom `reqwest::Client` injection guidance, blocking client variant behind feature flag, retry tuning examples.

  • `how-to/integrating-cycles-with-async-openai.md` — concrete composition of runcycles + async-openai: basic chat completion with token extraction from `response.usage`, cap application to `max_tokens`, streaming via `ReservationGuard` (with explicit handling of OpenAI's `stream_options.include_usage` requirement), OpenAI-vs-Cycles error separation, token-to-microcents helper for USD-denominated budgets, brief Anthropic variant via `anthropic-sdk-rs`. Gotchas section flags the five common stumbles.

Discoverability:

  • Rust quickstart's `## Configuration` section now links to the new config reference. Quickstart's `## Next steps` re-ordered to surface both new docs prominently.
  • Rust integration guide gets a callout near the top pointing at the async-openai doc for the real example.
  • VitePress sidebar (config.ts) updated: Rust client config added to the Configuration section; "Integrate with async-openai" added under the Rust how-to subsection.
  • `how-to/index.md` LLM providers row updated: was "OpenAI (Python) · OpenAI (TypeScript)" — now includes "OpenAI (Rust / async-openai)".

Glossary linker: ran on both new docs; added 7 contextual links.

What this PR doesn't fix (separate repo)

The cycles-client-rust repo itself has a parallel gap. Worth filing as a follow-up issue against that repo:

  • `/examples` directory has 5 files (basic, error_handling, guard, streaming, with_cycles) but no LLM integration example. Adding `examples/async_openai_completion.rs` and `examples/axum_middleware.rs` would close the same gap on the repo side.
  • The repo README's quickstart opens with abstract `call_llm()` rather than a real async-openai snippet. Same fix.

I cannot edit that repo from this PR — flagging it here so it's tracked.

Expected impact

The 907 → 67 conversion is what's measurable. If even a modest fraction of evaluators were bouncing because of the missing async-openai example or the missing config reference, this should narrow the ratio over the coming weeks. Not predicting a specific lift — Rust ecosystem culture (source-inspection over install) means the ratio will stay above 1:1 even with these fixes. But the trajectory should be observable in install-counts cache over the next 2–4 weeks.

Test plan

  • `npm run build` succeeds without broken-link warnings
  • Sidebar shows the new Rust config entry alongside Python/TS/Spring
  • Sidebar shows "Integrate with async-openai" under the Rust how-to subsection
  • `/configuration/rust-client-configuration-reference` renders correctly
  • `/how-to/integrating-cycles-with-async-openai` renders correctly
  • All cross-links from the new docs resolve
  • Rust quickstart's Next Steps section surfaces the new docs at the top

amavashev added 3 commits May 16, 2026 07:36
…ersion friction

Investigation of the cycles-client-rust 13:1 clone-to-install ratio (907
clones, 67 installs over 6 weeks). Surface quality is fine: crates.io
description is clean, docs.rs renders at 99.77%, README has badges +
cargo block above the quickstart, /examples has 5 files. The crate
itself is in good shape.

Two real gaps identified in the cycles-docs surface:

1. **Missing Rust client config reference.** Python, TypeScript, and
   Spring Boot all have `configuration/*-client-configuration-reference.md`
   docs. Rust did not. An evaluator hitting the docs to look up retry
   tuning, custom reqwest::Client wiring, the blocking variant, or env
   var prefix customization had nowhere to land.

2. **No real LLM-integration example.** The Rust quickstart and Rust
   integration guide both use `call_llm()` / `openai_call()` /
   `stream_llm_response()` as placeholders. An evaluator who copied the
   sample code had to invent the async-openai wiring themselves —
   including streaming token capture, OpenAI vs Cycles error mapping,
   and cap-to-max_tokens application. That's the friction point that
   most plausibly explains the ratio.

Changes:

- **NEW** `configuration/rust-client-configuration-reference.md` —
  CyclesConfig fields, env vars (default and custom prefix), builder
  API method reference, custom reqwest::Client guidance, blocking
  client variant behind feature flag, retry tuning examples. Verified
  against cycles-client-rust src/config.rs.

- **NEW** `how-to/integrating-cycles-with-async-openai.md` — concrete
  composition of `runcycles` with `async-openai`: basic chat completion
  with token extraction from `response.usage`, cap application to
  max_tokens, streaming via ReservationGuard with include_usage handling,
  OpenAI vs Cycles error separation, token-to-microcents helper, brief
  Anthropic variant via anthropic-sdk-rs. Gotchas section flags the
  five common stumbles (missing usage, None content, key leakage in
  metadata, runtime mismatch).

- **MOD** `quickstart/getting-started-with-the-rust-client.md` —
  Configuration section now links to the new config reference; Next
  Steps re-ordered to surface both new docs prominently.

- **MOD** `how-to/integrating-cycles-with-rust.md` — callout near the
  top pointing readers who want a real LLM example to the new
  async-openai doc.

- **MOD** `.vitepress/config.ts` — added Rust client config to the
  sidebar Configuration section; added async-openai entry under the
  Rust how-to subsection.

- **MOD** `how-to/index.md` — added "OpenAI (Rust / async-openai)" to
  the LLM providers row (previously listed only Python and TypeScript
  for OpenAI specifically).

Glossary linker ran on both new docs; added 7 contextual links across
them.

Out of scope (separate repo, can't edit from here):

- The cycles-client-rust /examples directory has 5 files but no LLM
  integration example. Adding examples/async_openai_completion.rs and
  examples/axum_middleware.rs would close a parallel gap on the repo
  side. Worth filing on cycles-client-rust as a follow-up issue.

- A README quickstart that opens with the async-openai example rather
  than the abstract `call_llm()` would compound this; same parallel
  gap on the repo side.
Codex flagged substantial code-accuracy errors that would have produced
non-compiling examples. Verified each fix against the live cycles-client-rust
source (src/config.rs, src/blocking.rs, src/lifecycle.rs) and async-openai
0.30.x docs.

Apply/skip tally: 12 applied, 0 pushed back.

Config reference fixes:
- Retry section: I described the retry engine as if it ships today.
  In runcycles 0.2.x, retry.rs is dead-code and ReservationGuard::commit
  does not retry. Reframed the section as "fields present on the struct
  for a future engine; no runtime effect in 0.2.x." Removed the
  "Disabling retry" and "Aggressive retry" examples that used builder
  methods (retry_initial_delay/multiplier/max_delay) that do not exist
  on CyclesClientBuilder.
- Programmatic CyclesConfig example: removed `..Default::default()`
  (CyclesConfig does not implement Default). Spell out all fields, or
  use the builder.
- Subject defaults: my "Resolution order" section claimed config-level
  subject defaults are auto-applied to request subjects. Verified
  against lifecycle.rs: with_cycles uses WithCyclesConfig.subject (or
  Subject::default()), NOT the CyclesConfig subject. Rewrote the
  section to clarify that subject fields are stored on the config but
  not auto-applied; recommended building one Subject and reusing it.
- Blocking client: I said it "mirrors the async client's surface."
  Verified against blocking.rs: only low-level protocol methods are
  exposed (create_reservation, commit_reservation, release_reservation,
  extend_reservation, decide, create_event, list_reservations,
  get_reservation, get_balances). No with_cycles, no ReservationGuard.
  Updated the section to list what is actually available.
- Blocking snippet: added missing `?` after `BlockingCyclesClient::new(...)`
  (it returns `Result<Self, Error>`) and imported `BalanceParams`.
- Builder method reference table: removed retry_initial_delay,
  retry_multiplier, retry_max_delay (not on builder); noted these are
  reachable only by constructing CyclesConfig directly.
- Removed "Unlike most clients in the corpus" internal-review tone.

async-openai doc fixes:
- "dominant Rust client" → "widely used".
- "only the type names change" → reframed as not-portable (the
  provider-side type paths differ in meaningful ways).
- Streaming code: I described needing
  `stream_options.include_usage = true` but never set it on the request.
  Added `.stream_options(ChatCompletionStreamOptions { include_usage: true })`
  to the request builder, imported the type.
- Error handling section: completely rewrote. with_cycles wraps closure
  errors as `Error::Validation(format!("guarded function failed: {e}"))`
  — the typed OpenAIError is stringified and lost. Switched the
  error-aware pattern to ReservationGuard so the typed OpenAIError
  reaches the caller. Added explicit guidance on when to use
  with_cycles vs ReservationGuard.
- Anthropic section: demoted from concrete code (using crate API names
  I had not verified) to a brief "Other Rust LLM clients" note that
  describes what to adapt without making specific API claims about
  anthropic-sdk-rust.
- Streaming usage setup moved INTO the code block (was only mentioned
  in the gotchas section).
- Glossary anchor `#tokens` → `#token` (the glossary heading is
  singular).

Other:
- how-to/integrating-cycles-with-rust.md:283: updated stale link from
  /quickstart/getting-started-with-the-rust-client#configuration to
  /configuration/rust-client-configuration-reference.

Sources verified against cycles-client-rust HEAD (src/config.rs,
src/blocking.rs, src/lifecycle.rs) and async-openai 0.30.1 docs.rs.
Apply/skip tally: 5 applied, 0 pushed back.

Applied:
- Config reference line 29: "applied to every request unless overridden"
  contradicted the corrected section at :153-174 and lifecycle.rs. Replaced
  with explicit "stored on the config but not auto-applied to per-request
  subjects in 0.2.x" + pointer to the detailed section.
- Config reference retry framing: narrowed "Transport or Api { 5xx }" to
  "transient commit failures (network, 5xx, timeouts) surface as Transport
  or Api{..}" since 4xx can also surface; softened "stable when the engine
  ships" to "documented here because they are already public" (avoids any
  implied semver promise about the future engine).
- Config reference blocking method list: added
  `create_reservation_with_metadata` (it IS public on BlockingCyclesClient
  per src/blocking.rs); added `config()` accessor, noted it's not a
  protocol method.
- async-openai error-handling pattern: `guard.release(...)` takes
  `impl Into<String>`, not a `ReleaseRequest`. Replaced
  `guard.release(ReleaseRequest::new(Some(...)))` with
  `guard.release(format!(...))`. Removed the now-unused `ReleaseRequest`
  import.
- async-openai line 366: "elsewhere in the corpus" → "elsewhere in the
  docs" (internal-review tone).

Codex verified against cycles-client-rust at runcycles 0.2.4 plus
async-openai 0.30.1 docs.rs.
@amavashev amavashev merged commit abdfc84 into main May 16, 2026
5 checks passed
@amavashev amavashev deleted the docs/rust-client-config-and-llm-integration branch May 16, 2026 15:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant