elph-ai
Unified LLM API with provider collections, automatic auth resolution, token and cost tracking, and simple context persistence and hand-off to other models mid-session. Rust port of @earendil-works/pi-ai.
Note: This library only includes models that support tool calling (function calling), as this is essential for agentic workflows.
Table of Contents
- Supported Providers
- Installation
- Quick Start
- Providers and Models
- Auth
- Tools
- Image Input
- Image Generation
- Thinking/Reasoning
- Stream Options
- Request Cancellation
- Stop Reasons
- Error Handling
- HTTP and WebSocket Proxies
- Custom Providers
- Faux Provider for Tests
- Cross-Provider Handoffs
- Context Serialization
- OAuth Providers
- Development
- License
Supported Providers
- OpenAI
- Ant Ling
- Azure OpenAI (Responses)
- OpenAI Codex (ChatGPT Plus/Pro subscription, requires OAuth)
- DeepSeek
- NVIDIA NIM
- Anthropic
- Vertex AI (Gemini via Vertex AI)
- Mistral
- Groq
- Cerebras
- Cloudflare AI Gateway
- Cloudflare Workers AI
- xAI
- OpenRouter
- Vercel AI Gateway
- ZAI Coding Plan (Global) (with separate China provider)
- MiniMax (with separate China provider)
- Together AI
- Hugging Face
- Moonshot AI (with separate China provider)
- GitHub Copilot (requires OAuth)
- Amazon Bedrock
- OpenCode Zen
- OpenCode Go
- Fireworks (OpenAI- and Anthropic-compatible APIs)
- Kimi For Coding (Moonshot AI subscription endpoint, Anthropic-compatible API)
- Xiaomi MiMo (API billing endpoint, with separate Token Plan providers for
cn/ams/sgp) - Any OpenAI-compatible API: Ollama, vLLM, LM Studio, etc. (via custom providers)
Image generation is currently available through OpenRouter (openrouter-images API).
Installation
Add to your Cargo.toml:
[]
= "0.0.21"
= { = "1", = ["macros", "rt-multi-thread"] }
Or from the workspace:
Quick Start
Build a Models collection of providers and stream through it. The quickest start registers every built-in provider; apps that only need a subset can register individual provider factories instead (see Provider Factories).
use ;
use ;
use ;
use json;
async
See examples/opencode_big_pickle.rs for a runnable example with progress output and streaming flags.
Providers and Models
A provider is the runtime unit: it owns its model catalog, its auth (API key resolution, OAuth flows), and its stream behavior. A Models collection holds providers and routes every request to the provider that owns the model.
Providers internally share API implementations (the wire protocols): Anthropic models use anthropic-messages, OpenAI uses openai-responses, while xAI, Groq, Cerebras, OpenRouter, and most others share openai-completions. Mixed-API providers (GitHub Copilot, OpenCode Zen, Fireworks) dispatch per model.
Nine chat APIs are registered in elph_ai::api::builtin_apis():
| API ID | Typical providers |
|---|---|
anthropic-messages |
Anthropic, Kimi For Coding, parts of Fireworks / GitHub Copilot |
openai-completions |
Groq, Cerebras, OpenRouter, DeepSeek, Ollama-compatible endpoints, etc. |
openai-responses |
OpenAI |
openai-codex-responses |
OpenAI Codex (ChatGPT subscription) |
azure-openai-responses |
Azure OpenAI |
google-generative-ai |
Google Gemini |
google-vertex |
Vertex AI |
mistral-conversations |
Mistral |
bedrock-converse-stream |
Amazon Bedrock |
Image generation uses a separate openrouter-images API (see Image Generation). Use api_for("anthropic-messages") or call modules under elph_ai::api for lower-level control.
Model catalogs are embedded as JSON under models/ and loaded at compile time via include_str!.
Provider Factories
For apps that only need specific providers, register individual factories from elph_ai::providers:
use ;
use ;
let mut models = create_models;
models.set_provider;
models.set_provider;
Additional factories live in elph_ai::providers::builtin (amazon_bedrock_provider, google_vertex_provider, github_copilot_provider, etc.). Use builtin_providers() to inspect the full list.
All Built-in Providers
use builtin_models;
let models = builtin_models; // every built-in provider registered
builtin_models() accepts the same options as create_models() (credentials, auth_context). builtin_providers() returns the provider list if you want to register them on your own collection.
Querying Models
Reads are synchronous and return the last-known lists:
let providers = models.get_providers;
let provider = models.get_provider;
let all = models.get_models;
let anthropic_models = models.get_models;
let model = models.get_model;
for m in anthropic_models
Narrow dynamically looked-up models with has_api() when you need API-specific option typing:
use has_api;
if let Some = models.get_model
Static Catalog Reads
For tooling that wants the embedded built-in catalog independent of any collection:
use ;
let model = get_builtin_model;
let providers = get_builtin_providers;
let anthropic = get_builtin_models;
Image model catalogs are separate:
use get_builtin_image_models;
let openrouter_images = get_builtin_image_models;
Dynamic Providers
Providers may have dynamic model lists (a llama.cpp server, a live OpenRouter listing). Reads stay sync; fetching is an explicit async verb:
// get_models() returns the last-known list (empty before the first refresh)
models.refresh.await?; // one provider; rejects on failure
models.refresh.await?; // all providers concurrently, best-effort
let fresh = models.get_model;
Static built-in providers are no-ops for refresh(). See Custom Providers for building a dynamic provider.
Auth
Every provider owns its auth: how API keys resolve (stored credentials, environment variables, ambient sources like AWS profiles or gcloud ADC) and, where supported, OAuth login/refresh flows.
How Auth Resolves
When you call models.stream(), the collection resolves auth through the owning provider and merges it into the request. Explicit per-request values always win:
// Resolved through the provider (env var, stored credential, OAuth token):
models.complete.await;
// Explicit key wins over anything the provider would resolve:
models.complete
.await;
Inspect resolution without making a request — useful for status UIs:
let auth = models.get_auth.await?;
if let Some = &auth else
get_auth() returns Ok(None) for unconfigured providers and Err(ModelsError) when something is broken (ModelsErrorCode::Oauth: token refresh failed; ModelsErrorCode::Auth: key resolution or credential store failure). Request paths surface the same failures as stream errors.
Credential Store
Stored credentials (API keys entered interactively, OAuth tokens) live in a CredentialStore — one type-tagged credential per provider. elph-ai ships an in-memory default; apps inject persistent storage:
use ;
use Arc;
let models = create_models;
The contract is small: read(provider_id), modify(provider_id, fn) (serialized read-modify-write), and delete(provider_id). OAuth token refresh runs inside modify, so concurrent requests cannot double-refresh a rotated token. A stored credential owns its provider: environment variables are only consulted when nothing is stored.
API-key credentials can carry provider-scoped env/config values:
Environment Variables
Built-in providers resolve these environment variables:
| Provider | Environment Variable(s) |
|---|---|
| OpenAI | OPENAI_API_KEY |
| Ant Ling | ANT_LING_API_KEY |
| Azure OpenAI | AZURE_OPENAI_API_KEY + AZURE_OPENAI_BASE_URL or AZURE_OPENAI_RESOURCE_NAME. Optional: AZURE_OPENAI_API_VERSION, AZURE_OPENAI_DEPLOYMENT_NAME_MAP |
| Anthropic | ANTHROPIC_API_KEY or ANTHROPIC_OAUTH_TOKEN |
| DeepSeek | DEEPSEEK_API_KEY |
| NVIDIA NIM | NVIDIA_API_KEY |
GEMINI_API_KEY |
|
| Vertex AI | GOOGLE_CLOUD_API_KEY or GOOGLE_CLOUD_PROJECT (or GCLOUD_PROJECT) + GOOGLE_CLOUD_LOCATION + ADC |
| Mistral | MISTRAL_API_KEY |
| Groq | GROQ_API_KEY |
| Cerebras | CEREBRAS_API_KEY |
| Cloudflare AI Gateway | CLOUDFLARE_API_KEY + CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_GATEWAY_ID |
| Cloudflare Workers AI | CLOUDFLARE_API_KEY + CLOUDFLARE_ACCOUNT_ID |
| xAI | XAI_API_KEY |
| Fireworks | FIREWORKS_API_KEY |
| Together AI | TOGETHER_API_KEY |
| OpenRouter | OPENROUTER_API_KEY |
| Vercel AI Gateway | VERCEL_AI_GATEWAY_API_KEY |
| ZAI Coding Plan (Global) | ZAI_API_KEY |
| ZAI Coding Plan (China) | ZAI_CODING_CN_API_KEY |
| MiniMax (Global) | MINIMAX_API_KEY |
| MiniMax (China) | MINIMAX_CN_API_KEY |
| Moonshot AI / Moonshot AI (China) | MOONSHOT_API_KEY |
| Hugging Face | HF_TOKEN |
| OpenCode Zen / OpenCode Go | OPENCODE_API_KEY |
| Kimi For Coding | KIMI_API_KEY |
| Xiaomi MiMo (API billing) | XIAOMI_API_KEY |
| Xiaomi MiMo Token Plan (China/AMS/SGP) | XIAOMI_API_KEY |
| GitHub Copilot | COPILOT_GITHUB_TOKEN |
| Amazon Bedrock | AWS_REGION or AWS_DEFAULT_REGION, AWS_PROFILE, AWS_BEARER_TOKEN_BEDROCK (bearer auth path). Optional: AWS_BEDROCK_FORCE_CACHE=1, ELPH_CACHE_RETENTION=long |
Amazon Bedrock also resolves ambient AWS credentials (access key pairs, ECS task roles, web identity tokens) when no bearer token is set. Vertex AI resolves either an explicit key or gcloud Application Default Credentials plus project/location.
Per-request StreamOptions.env and stored credential env maps override process environment for the same keys. Global tuning variables:
| Variable | Effect |
|---|---|
ELPH_CACHE_RETENTION=long |
Default prompt cache retention to long when cache_retention is unset (Anthropic, OpenAI Responses, Bedrock Claude models) |
HTTP_PROXY / HTTPS_PROXY / ALL_PROXY |
HTTP(S) proxy for outbound provider requests (see HTTP and WebSocket Proxies) |
NO_PROXY |
Hostnames/ports to bypass the proxy (*, comma-separated, optional :port suffix) |
Tools
Tools enable LLMs to interact with external systems. Tool parameters are JSON Schema values (serde_json::Value), validated with the jsonschema crate.
Defining Tools
use Tool;
use json;
let weather_tool = Tool ;
For Google API compatibility, prefer enum arrays over complex anyOf/const patterns in schemas.
Handling Tool Calls
Tool results use content blocks and can include both text and images:
use ;
let response = models.complete.await;
for block in &response.content
// Tool results can also include images (for vision-capable models)
context.messages.push;
Streaming Tool Calls with Partial JSON
During streaming, tool call arguments are progressively parsed as they arrive:
let stream = models.stream;
let mut events = stream.into_stream;
while let Some = events.next.await
Important notes about partial tool arguments:
- During
ToolcallDeltaevents,argumentscontains the best-effort parse of partial JSON - Fields may be missing or incomplete — always check for existence before use
- At minimum,
argumentswill be an empty object, never missing - The Google provider does not support function call streaming; you receive a single
ToolcallDeltawith full arguments
Validating Tool Arguments
Use validate_tool_call before executing tools:
use validate_tool_call;
if let ToolcallEnd = event
Complete Event Reference
| Event | Description | Key fields |
|---|---|---|
Start |
Stream begins | partial |
TextStart |
Text block starts | content_index |
TextDelta |
Text chunk received | delta, content_index |
TextEnd |
Text block complete | content, content_index |
ThinkingStart |
Thinking block starts | content_index |
ThinkingDelta |
Thinking chunk received | delta, content_index |
ThinkingEnd |
Thinking block complete | content, content_index |
ToolcallStart |
Tool call begins | content_index |
ToolcallDelta |
Tool arguments streaming | delta, partial.content[content_index].arguments |
ToolcallEnd |
Tool call complete | tool_call |
Done |
Stream complete | reason, message |
Error |
Stream failed | reason, error |
Events serialize as snake_case JSON (text_delta, toolcall_end, etc.) via serde.
Image Input
User messages support text and image content blocks for vision-capable models:
let context = Context ;
Image Generation
Image generation uses a separate API surface from text/chat generation: an ImagesModels collection holds ImagesProviders, reads are sync, and auth resolves through the owning provider. Image generation is one-shot — use generate_images(), not the chat/stream APIs.
use ;
use get_builtin_image_models;
let images = builtin_images_models;
let model = get_builtin_image_models
.into_iter
.find
.unwrap;
let result = images
.generate_images
.await;
for block in &result.output
Check capabilities on model metadata:
println!; // ["text", "image"]
println!; // ["image"] or ["image", "text"]
Failures return an AssistantImages with stop_reason: Error rather than panicking.
ImagesOptions mirrors chat StreamOptions for auth, timeouts, retries, proxy env, payload/response hooks, and cancellation:
use ImagesOptions;
use CancellationToken;
let token = new;
// cancel token when the user dismisses the UI
images.generate_images
.await;
Thinking/Reasoning
Many models support thinking/reasoning. Check model.reasoning; options passed to non-reasoning models are silently ignored.
Unified Interface (stream_simple / complete_simple)
use ;
let response = models
.complete_simple
.await;
for block in &response.content
Use get_supported_thinking_levels() and clamp_thinking_level() to respect per-model capability maps.
Provider-Specific Options (stream / complete)
stream() / complete() accept the owning API's full StreamOptions. Use has_api() to narrow models before passing API-specific fields (thinking_enabled, reasoning_effort, etc.).
Streaming Thinking Content
while let Some = events.next.await
Stream Options
stream() / complete() accept StreamOptions; stream_simple() / complete_simple() wrap the same fields in SimpleStreamOptions.base plus reasoning knobs.
| Field | Purpose |
|---|---|
temperature, max_tokens |
Sampling controls (max_tokens is clamped to remaining context) |
api_key |
Explicit key; overrides provider auth resolution |
transport |
Transport hint (sse, websocket, websocket-cached, auto) — used by OpenAI Codex when calling the API directly |
cache_retention |
Prompt cache retention (none, short, long); defaults from ELPH_CACHE_RETENTION |
session_id |
Stable session id (Codex WebSocket context reuse, request tracing headers) |
headers |
Per-request header overrides (None removes a model default header) |
timeout_ms, websocket_connect_timeout_ms |
HTTP and Codex WebSocket connect timeouts |
max_retries, max_retry_delay_ms |
Retry policy for transient failures |
metadata |
Opaque JSON attached to provider payloads where supported |
env |
Scoped environment map (proxy vars, Bedrock region, provider config) |
on_payload |
Async hook to inspect or rewrite the outgoing JSON body |
on_response |
Async hook invoked with HTTP status/headers after the provider responds |
signal |
CancellationToken for cooperative abort (see Request Cancellation) |
Provider-specific option structs (AnthropicOptions, OpenAICompletionsOptions, BedrockOptions, etc.) extend base: StreamOptions when you call elph_ai::api modules directly.
Chat streams parse provider SSE incrementally — events are emitted as chunks arrive rather than buffering the full response body first.
Request Cancellation
Pass a tokio_util::sync::CancellationToken in StreamOptions.signal (or SimpleStreamOptions.base.signal / ImagesOptions.signal). When cancelled:
- In-flight HTTP requests and SSE parsers stop promptly
- The final
AssistantMessage/AssistantImagesusesstop_reason: Aborted - Mid-stream partial content may be preserved on the final message when cancellation happens during generation
use CancellationToken;
let token = new;
let stream = models.stream;
// elsewhere: token.cancel();
let message = stream.result.await;
assert_eq!;
Cancellation is checked before the request is sent, while waiting on the network, and between SSE/WebSocket events. See tests/abort.rs and tests/sse_abort.rs.
Stop Reasons
| Reason | Meaning |
|---|---|
Stop |
Natural completion |
Length |
Hit max_tokens |
ToolUse |
Model wants to call tools |
Error |
Request failed |
Aborted |
Request was cancelled |
Error Handling
Request failures do not panic out of stream functions. Errors arrive as AssistantMessageEvent::Error and the final message carries details:
while let Some = events.next.await
let message = stream.result.await;
if matches!
Debugging Provider Payloads
Use the on_payload callback in StreamOptions to inspect the request payload sent to the provider:
use ;
let options = StreamOptions ;
Supported by stream, complete, stream_simple, and complete_simple.
Use on_response to capture raw HTTP metadata (status, headers) without logging full bodies:
use ProviderResponse;
use wrap_on_response;
let options = StreamOptions ;
HTTP and WebSocket Proxies
Outbound HTTP(S) provider traffic respects standard proxy environment variables, including values supplied through StreamOptions.env / stored credential env maps:
HTTP_PROXY,HTTPS_PROXY,ALL_PROXY— must behttp://orhttps://URLs (SOCKS and PAC are not supported)NO_PROXY— comma- or whitespace-separated hostnames; prefix with.or*for suffix/wildcard matches; optional:port
WebSocket URLs (ws://, wss://) map to http:// / https:// for proxy rule lookup. OpenAI Codex WebSocket transport tunnels through HTTPS proxies (CONNECT + nested TLS). See tests/http_proxy.rs and tests/codex_websocket_proxy.rs.
Custom Providers
Build providers with create_provider():
use ;
use openai_completions_api;
let provider = create_provider;
Call API implementations directly from elph_ai::api for lower-level control, or register custom providers on a Models collection.
Faux Provider for Tests
faux_provider() builds an in-memory provider with scripted responses:
use ;
use json;
let faux = faux_provider;
let mut models = create_models;
models.set_provider;
let model = faux.provider.get_models.clone;
faux.set_responses;
Notes:
- Responses are consumed from a queue in request order
- An empty queue returns an assistant error:
"No more faux responses queued" - Use
set_responses()to replace the queue andappend_responses()to extend it - Tool call arguments stream incrementally via
ToolcallDeltaevents
See tests/faux_provider.rs for integration coverage.
Cross-Provider Handoffs
The library supports seamless handoffs between providers within the same conversation. When messages from one provider are sent to another, transform_messages adapts them for compatibility:
- User and tool result messages pass through unchanged
- Assistant messages from the same provider/API are preserved as-is
- Assistant messages from different providers have thinking blocks converted to
<thinking>tagged text - Tool calls and regular text are preserved unchanged
use ;
let mut models = create_models;
models.set_provider;
models.set_provider;
// register additional providers as needed
let mut context = Context ;
let claude = models.get_model.unwrap;
context.messages.push;
context.messages.push;
let gpt = models.get_model.unwrap;
context.messages.push;
context.messages.push;
See tests/transform_messages.rs.
Context Serialization
Context, Message, and AssistantMessage derive Serialize/Deserialize. Contexts are plain JSON-friendly structs you can persist, send over the wire, and resume with a different model:
let json = to_string?;
let restored: Context = from_str?;
let response = models.complete.await;
OAuth Providers
Built-in OAuth flows are available for Anthropic, GitHub Copilot, and OpenAI Codex via elph_ai::auth::oauth:
use ;
use Arc;
register_oauth_provider;
let tokens = login_anthropic.await?;
Use get_oauth_api_key(), refresh_oauth_token(), and the CredentialStore to persist tokens across sessions.
OpenAI Codex Transport
OpenAI Codex models (openai-codex-responses API) support SSE and WebSocket transports with automatic fallback when connection limits are hit. Collection-level models.stream() defaults to auto (WebSocket with cached context when session_id is set, SSE fallback on limit errors).
For explicit transport control, call the API module directly:
use CodexTransport;
use ;
let api = OpenAICodexResponsesApi;
let _stream = api.stream_with_options;
CodexTransport values: Auto, Sse, WebSocket, WebSocketCached. Debug helpers (get_codex_websocket_debug_stats, close_codex_websocket_sessions) are exported from the crate root.
Vertex AI
Vertex AI supports either an API key or Application Default Credentials:
# Local ADC
# CI/Production
Development
Regenerating Model Catalogs
Chat and image model catalogs are generated from pi-ai scripts:
# From the repo root (requires upstream catalog checkout and npm deps)
# Or directly:
# Convert existing catalog output without re-running npm scripts:
Subcommands:
| Command | Catalog npm script | Output |
|---|---|---|
chat |
generate-models |
models/*.json + src/models/catalog.rs |
image |
generate-image-models |
models/images/*.json + src/images/models.rs |
test-image |
generate-test-image |
tests/data/red-circle.png |
all |
all of the above | everything |
Adding a New Provider
- Types (
src/types/mod.rs) — add API/provider identifiers and options if needed - API (
src/api/<api-id>.rs) — implementstream/stream_simplefor new wire protocols - Catalog — add fetch logic to the upstream catalog
generate-modelsscript, then runmake generate-models - Provider factory (
src/providers/builtin.rs) — wire catalog + auth + API adapter; register inbuiltin_providers() - Tests (
tests/) — streaming, tools, auth, cross-provider handoff as applicable
Running Tests
# Unit and integration tests (default — skips #[ignore] live tests)
# Run all tests including live provider tests (requires API keys)
# Individual live test binaries
Integration tests mirror upstream coverage: provider auth, SSE parsing and mid-stream abort, HTTP/WebSocket proxy routing, tool schemas, retry/overflow, OAuth, Bedrock endpoint resolution, Codex WebSocket transport, faux provider, and more under tests/.
License
Licensed under the MIT License.