Skip to content

Configuration

Liter-llm reads configuration from three sources, applied in priority order (lower numbers win):

  1. Constructor arguments — passed directly to the client factory (create_client(...) / ClientConfigBuilder).
  2. JSON stringcreate_client_from_json(json) for bindings without a builder.
  3. TOML fileliter-llm.toml, auto-discovered by walking up from the current working directory.

The Rust core exposes the same surface to every language binding via the C FFI. Methods like chat, embed, etc. are called on the client instance returned by the factory; there is no top-level LlmClient class to subclass or pre-configure in the bindings.

Place a liter-llm.toml file in your project directory. FileConfig::discover() (Rust) and the proxy server walk up the directory tree looking for it.

api_key = "sk-..."
base_url = "https://api.openai.com/v1"
model_hint = "openai"
timeout_secs = 120
max_retries = 5
cooldown_secs = 30
health_check_secs = 60
cost_tracking = true
tracing = true
[cache]
max_entries = 512
ttl_seconds = 600
backend = "memory"
[budget]
global_limit = 50.0
enforcement = "hard"
[budget.model_limits]
"openai/gpt-4o" = 25.0
[rate_limit]
rpm = 60
tpm = 100000
window_seconds = 60
[[providers]]
name = "my-provider"
base_url = "https://my-llm.example.com/v1"
auth_header = "Bearer"
model_prefixes = ["my-provider/"]
Field Type Description
api_key string Provider API key. Wrapped in SecretString internally.
base_url string Override the provider’s base URL.
model_hint string Pre-resolve a provider (e.g. "openai"); skips prefix lookup.
timeout_secs int Per-request timeout in seconds (default: 60).
max_retries int Retries on 429/5xx with exponential backoff (default: 3).
cooldown_secs int Circuit-breaker cooldown after transient errors.
health_check_secs int Interval for background health checks.
cost_tracking bool Enable per-request cost calculation.
tracing bool Emit tracing spans for each request (see Observability).
extra_headers map Additional headers attached to every outgoing request.
Field Type Description
max_entries int Maximum cached responses (default: 256).
ttl_seconds int Time-to-live for each entry in seconds (default: 300).
backend string "memory" (default) or an OpenDAL scheme ("redis", "s3", "fs", "gcs", …).
backend_config map OpenDAL backend-specific key-value config.

The backend and backend_config fields are only honored when liter-llm is compiled with the opendal-cache feature.

Field Type Description
global_limit float Maximum total spend in USD across all models.
model_limits map Per-model spend limits (model name → USD).
enforcement string "hard" (reject over-budget) or "soft" (warn only).
Field Type Description
rpm int Maximum requests per window.
tpm int Maximum tokens per window.
window_seconds int Window duration in seconds (default: 60).

Array of custom provider definitions. Each entry contains:

Field Type Description
name string Unique provider name.
base_url string Provider’s API base URL.
auth_header string Auth scheme (optional).
model_prefixes string[] Model name prefixes that route to this provider.

The constructors below match the actual binding surface. Other bindings (Go, Java, Kotlin Android, C#, Ruby, PHP, Elixir, Dart, Swift, Zig, WebAssembly) expose the same scalar arguments through their generated wrappers; JVM Kotlin applications use the Java binding from Kotlin. Use the matching API page under Reference for exact language signatures.

use liter_llm::{ClientConfigBuilder, DefaultClient};
use std::time::Duration;
let config = ClientConfigBuilder::new("sk-...")
.base_url("https://api.openai.com/v1")
.timeout(Duration::from_secs(120))
.max_retries(5)
.build();
let client = DefaultClient::new(config, Some("openai"))?;

Load from a liter-llm.toml:

use liter_llm::{FileConfig, DefaultClient};
if let Some(file) = FileConfig::discover()? {
let config = file.into_builder().build();
let client = DefaultClient::new(config, None)?;
}

ManagedClient::new(config, model_hint) is available with the tower feature and wires the full middleware stack (cache, budget, rate limit, cooldown, health, hooks, tracing) from the same ClientConfig.

import asyncio
import os
from liter_llm import create_client
from liter_llm._internal_bindings import ChatCompletionRequest
async def main() -> None:
client = create_client(
api_key=os.environ["OPENAI_API_KEY"],
base_url=None, # override provider base URL
model_hint="openai", # pre-resolve provider at construction
max_retries=3, # retry on transient failures
timeout_secs=60, # request timeout in seconds
)
request = ChatCompletionRequest.from_json(
'{"model":"openai/gpt-4o","messages":[{"role":"user","content":"Hello!"}]}'
)
response = await client.chat(request)
print(response.choices[0].message.content)
asyncio.run(main())
Option Type Default Description
api_key string required Provider API key. Wrapped in SecretString internally.
base_url string from registry Override the provider’s base URL.
model_hint string none Pre-resolve a provider at construction (e.g. "openai").
timeout_secs int 60 Request timeout in seconds.
max_retries int 3 Retries on 429/5xx responses with exponential backoff.

API keys passed to the constructor are wrapped in secrecy::SecretString. They are never logged, serialized, or included in error messages. If the constructor is given an empty api_key, the builder reads the standard environment variable for the active provider:

Provider Environment variable
OpenAI OPENAI_API_KEY
Anthropic ANTHROPIC_API_KEY
Google (Gemini) GEMINI_API_KEY
Groq GROQ_API_KEY
Mistral MISTRAL_API_KEY
Cohere CO_API_KEY
AWS Bedrock AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY

Override base_url to point at a local inference server or a corporate proxy:

# Ollama running locally
base_url = "http://localhost:11434/v1"
let config = ClientConfigBuilder::new("unused")
.base_url("http://localhost:11434/v1")
.build();

The response cache is wired through the Tower middleware stack. In Rust, attach CacheLayer directly; in other languages, configure it via TOML (and use ManagedClient indirectly through the bindings’ high-level clients).

[cache]
max_entries = 256
ttl_seconds = 300
backend = "memory"

With the opendal-cache feature, the cache backend can be any OpenDAL service — Redis, S3, GCS, Azure Blob, local filesystem, and more.

[cache]
ttl_seconds = 3600
backend = "redis"
[cache.backend_config]
endpoint = "redis://localhost:6379"

Track and enforce spending limits per model and globally. Costs are recomputed after every successful response using liter_llm::cost::completion_cost.

[budget]
global_limit = 10.0
enforcement = "hard"
[budget.model_limits]
"openai/gpt-4o" = 5.0

Enforcement::Hard rejects requests with LiterLlmError::BudgetExceeded once the limit is reached. Enforcement::Soft emits a tracing::warn! but allows the request through.

Per-model RPM (requests per minute) and TPM (tokens per minute) limits using a fixed window.

[rate_limit]
rpm = 60
tpm = 100000
window_seconds = 60

Hooks are implemented via the Rust LlmHook trait and are wired into the Tower stack through HooksLayer. They are not currently exposed through the language bindings.

use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use liter_llm::error::{LiterLlmError, Result};
use liter_llm::tower::hooks::{HooksLayer, LlmHook};
use liter_llm::tower::types::{LlmRequest, LlmResponse};
struct LoggingHook;
impl LlmHook for LoggingHook {
fn on_request(&self, _req: &LlmRequest)
-> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>
{
Box::pin(async {
tracing::info!("dispatching request");
Ok(())
})
}
fn on_response(&self, _req: &LlmRequest, _resp: &LlmResponse)
-> Pin<Box<dyn Future<Output = ()> + Send + '_>>
{
Box::pin(async {
tracing::info!("response received");
})
}
fn on_error(&self, _req: &LlmRequest, err: &LiterLlmError)
-> Pin<Box<dyn Future<Output = ()> + Send + '_>>
{
let message = err.to_string();
Box::pin(async move {
tracing::error!(error = %message, "request failed");
})
}
}
let hooks: Vec<Arc<dyn LlmHook>> = vec![Arc::new(LoggingHook)];
let layer = HooksLayer::new(hooks);

Returning Err from on_request short-circuits the service chain, so hooks can implement guardrails (content filtering, budget gating, etc.).

Custom providers are registered in a process-wide global registry. Once registered, any request whose model name matches one of the provider’s model_prefixes is routed there.

use liter_llm::{register_custom_provider, CustomProviderConfig, AuthHeaderFormat};
register_custom_provider(CustomProviderConfig {
name: "my-provider".into(),
base_url: "https://my-llm.example.com/v1".into(),
auth_header: AuthHeaderFormat::Bearer,
model_prefixes: vec!["my-provider/".into()],
})?;

Unregister with unregister_custom_provider("my-provider") (returns Result<bool>: Ok(true) if a provider with that name existed).

AuthHeaderFormat has three variants: Bearer (sends Authorization: Bearer <key>), ApiKey(header_name) (sends a custom header), and None (no auth header). Note that FileProviderConfig::auth_header (from TOML) is a raw header-name string, not an AuthHeaderFormat; map it to the enum yourself as shown above.

The tracing reference has moved to Observability. That page covers span attributes, OTEL exporter setup, cost tracking, and Tower layer composition.