llmleaf-client (Rust)
Async Rust SDK for the llmleaf LLM proxy.
llmleaf speaks OpenAI/OpenRouter-shaped JSON over HTTP, so the public types are plain serde
structs and the transport is reqwest over rustls (no system OpenSSL). The
proto is the source of truth: build.rs compiles it with
prost-build on every build and exposes the messages under pb as a codegen check — the
serde types at the crate root are what you actually use.
Install
This crate is its own standalone workspace inside the monorepo (intentionally not a member of the root workspace). Depend on it by path or git:
[]
= { = "https://github.com/codefionn/llmleaf", = "main" }
= { = "1", = ["macros", "rt-multi-thread"] }
= "0.3"
You need protoc (libprotoc 35) on PATH at build time — prost shells out to it.
Example
use StreamExt;
use ;
async
OpenAI Responses dialect
POST /v1/responses is the same canonical core behind a different edge dialect. input is a
bare string (one user message) or an array of items; use ResponseItem to build multi-turn
input (messages, function_call / function_call_output replay, reasoning items). llmleaf is
stateless, so the response always reports "store": false.
use StreamExt;
use ;
// Non-streaming — bare-string input.
let resp = client
.responses
.await?;
println!;
// Multi-turn item array (replaying a tool call and its result).
let req = new;
// Streaming — typed events, NO [DONE] sentinel: the stream ends on the terminal
// response.completed / .incomplete / .failed event. Accumulate output_text deltas.
let mut events = client.responses_stream.await?;
while let Some = events.next.await
Need a timeout, an admin token, or your own reqwest::Client? Use the builder:
use Duration;
let client = builder
.timeout
.admin_token // adds per-model `endpoints` to GET /v1/models
.build?;
Endpoints
| Call | Method | Notes |
|---|---|---|
| Chat | chat / chat_stream |
stream yields ChatCompletionChunk, stops on [DONE] |
| Responses | responses / responses_stream |
stream yields ResponsesStreamEvent, ends on the terminal event (no [DONE]) |
| Embeddings | embeddings |
decodes base64 vectors → Vec<f32> |
| Models | list_models |
type filter + search |
| Speech (TTS) | speech |
returns (bytes, content_type) |
| Voices | voices |
|
| Transcribe (STT) | transcribe |
multipart; Transcription::Json or ::Text |
| Batches | create_batch / get_batch / cancel_batch / batch_results |
results stream BatchResultLine |
Gateway errors come back as Error::Api { status, message }, parsed from
{"error":{"message":"..."}}.
Run the example
It lists models, does a non-streaming chat, streams one, then does the same pair over the Responses dialect (printing deltas live).
Regenerate from the proto
Codegen is wired into build.rs, so a build is a regeneration — after editing
../proto/llmleaf/v1/llmleaf.proto, just rebuild
(cargo build, or make gen-rust from clients/).
Notes
- The
pb(prost) types are the codegen proof, not the wire types — use the serde types at the crate root for anything over HTTP. - Free-form JSON fields (
extra,ResponseFormat.json_schema, …) are spliced verbatim and not validated (intentional passthrough);ChatRequest.extrakeys merge at the top level. - No retries/backoff. The realtime WebSocket surface is out of scope.
License
Dual-licensed under Apache-2.0 or MIT at your option. Copyright (C) 2026 Fionn Langhans.