-
Notifications
You must be signed in to change notification settings - Fork 4.4k
05.2 Custom Providers
Relevant source files
The following files were used as context for generating this wiki page:
This page documents how to configure custom LLM API endpoints in ZeroClaw. Custom providers enable you to use any OpenAI-compatible or Anthropic-compatible service as your LLM backend.
For information about built-in providers (OpenRouter, OpenAI, Anthropic, etc.), see Built-in Providers. For provider resilience features (retries, fallbacks), see Provider Resilience.
ZeroClaw supports two custom provider prefixes:
| Prefix | API Format | Use Case |
|---|---|---|
custom: |
OpenAI /v1/chat/completions
|
Any OpenAI-compatible service (local LLMs, cloud gateways, custom inference servers) |
anthropic-custom: |
Anthropic /v1/messages
|
Services implementing the Anthropic Messages API format |
Custom providers are first-class citizens in the provider ecosystem. They receive the same resilience wrappers (retries, fallbacks, rate limiting) as built-in providers.
Sources: src/providers/mod.rs:744-770, docs/custom-providers.md:1-112
flowchart TD
Config["Config: default_provider"] --> ParsePrefix["Parse Provider Name"]
ParsePrefix --> CheckCustom{"Starts with\n'custom:' or\n'anthropic-custom:'?"}
CheckCustom -->|No| BuiltIn["Route to Built-in Provider\n(OpenRouter, OpenAI, etc.)"]
CheckCustom -->|Yes| ExtractURL["Extract URL Suffix"]
ExtractURL --> ValidateURL["parse_custom_provider_url()"]
ValidateURL --> CheckScheme{"Valid HTTP/HTTPS?"}
CheckScheme -->|No| Error["anyhow::bail\nInvalid URL"]
CheckScheme -->|Yes| DetermineType{"Provider Type?"}
DetermineType -->|custom:| CreateCompatible["OpenAiCompatibleProvider::new\nAuthStyle::Bearer"]
DetermineType -->|anthropic-custom:| CreateAnthropic["AnthropicProvider::with_base_url"]
CreateCompatible --> ResolveCredential["resolve_provider_credential()"]
CreateAnthropic --> ResolveCredential
ResolveCredential --> WrapReliable["ReliableProvider::new\n(retries + fallbacks)"]
WrapReliable --> Return["Return Box<dyn Provider>"]
style ValidateURL fill:#f9f9f9
style ResolveCredential fill:#f9f9f9
style WrapReliable fill:#f9f9f9
Diagram: Custom Provider Instantiation Flow
The factory function create_provider_with_url in src/providers/mod.rs:592-778 handles custom provider creation. The flow:
-
Prefix Detection: Lines 744 and 760 check for
custom:andanthropic-custom:prefixes -
URL Extraction:
strip_prefix()removes the prefix to get the base URL -
Validation:
parse_custom_provider_url()src/providers/mod.rs:549-570 validates URL format and scheme -
Provider Construction: Instantiates either
OpenAiCompatibleProviderorAnthropicProviderwith the custom base URL -
Credential Resolution:
resolve_provider_credential()src/providers/mod.rs:465-547 finds the API key -
Resilience Wrapping: The provider is wrapped in
ReliableProviderbycreate_resilient_provider()src/providers/mod.rs:796-840
Sources: src/providers/mod.rs:592-778, src/providers/mod.rs:549-570, src/providers/mod.rs:796-840
flowchart TD
Input["Raw URL String\ne.g., 'https://api.example.com'"]
Input --> Trim["Trim Whitespace"]
Trim --> CheckEmpty{"Empty?"}
CheckEmpty -->|Yes| ErrEmpty["Error:\nURL required"]
CheckEmpty -->|No| Parse["reqwest::Url::parse()"]
Parse --> ParseOk{"Parse Success?"}
ParseOk -->|No| ErrFormat["Error:\nInvalid URL format"]
ParseOk -->|Yes| CheckScheme["Extract scheme()"]
CheckScheme --> ValidScheme{"http:// or https://?"}
ValidScheme -->|No| ErrScheme["Error:\nMust be http:// or https://"]
ValidScheme -->|Yes| Success["Return Validated URL"]
style Parse fill:#f9f9f9
style Success fill:#e8f5e9
Diagram: URL Validation Process
The parse_custom_provider_url() function src/providers/mod.rs:549-570 enforces these constraints:
- Non-empty: Rejects blank strings
-
Valid URL syntax: Uses
reqwest::Url::parse()to validate structure - HTTP/HTTPS only: Rejects other schemes (file://, ftp://, etc.)
Validation errors include helpful format hints like "custom:https://your-api.com" to guide users.
Sources: src/providers/mod.rs:549-570
flowchart TD
Start["resolve_provider_credential(name, api_key_override)"]
Start --> CheckOverride{"api_key parameter\nprovided?"}
CheckOverride -->|Yes, non-empty| UseOverride["Return explicit\nAPI key"]
CheckOverride -->|No or empty| CheckProviderEnv["Check provider-specific\nenv vars"]
CheckProviderEnv --> EnvMapping["Environment Variable Mapping:\n- anthropic → ANTHROPIC_OAUTH_TOKEN,\n ANTHROPIC_API_KEY\n- openrouter → OPENROUTER_API_KEY\n- openai → OPENAI_API_KEY\n- etc."]
EnvMapping --> ProviderEnvFound{"Found?"}
ProviderEnvFound -->|Yes| UseProviderEnv["Return provider-specific\nenv var value"]
ProviderEnvFound -->|No| CheckGenericEnv["Check generic env vars:\n1. ZEROCLAW_API_KEY\n2. API_KEY"]
CheckGenericEnv --> GenericFound{"Found?"}
GenericFound -->|Yes| UseGeneric["Return generic\nenv var value"]
GenericFound -->|No| ReturnNone["Return None\n(no credential)"]
style UseOverride fill:#e8f5e9
style UseProviderEnv fill:#e8f5e9
style UseGeneric fill:#e8f5e9
style ReturnNone fill:#ffebee
Diagram: Credential Resolution Hierarchy
For custom providers (custom: and anthropic-custom:), credential resolution follows this priority order implemented in resolve_provider_credential() src/providers/mod.rs:465-547:
| Priority | Source | Example |
|---|---|---|
| 1 | Explicit api_key parameter in config.toml |
api_key = "sk-..." |
| 2 | Generic ZEROCLAW_API_KEY env var |
export ZEROCLAW_API_KEY="sk-..." |
| 3 | Generic API_KEY env var |
export API_KEY="sk-..." |
Note: Unlike built-in providers (which check provider-specific env vars like OPENAI_API_KEY), custom providers only check the generic fallback variables. This is because the system cannot know which provider-specific variable to check when the provider name is arbitrary (e.g., custom:https://mycompany.ai).
Sources: src/providers/mod.rs:465-547, src/providers/mod.rs:537-545
classDiagram
class OpenAiCompatibleProvider {
+String name
+String base_url
+Option~String~ credential
+AuthStyle auth_header
+bool supports_responses_fallback
+Option~String~ user_agent
+new(name, base_url, credential, auth_style)
+chat_completions_url() String
+responses_url() String
+apply_auth_header(req, credential) RequestBuilder
}
class AuthStyle {
<<enumeration>>
Bearer
XApiKey
Custom(String)
}
class Provider {
<<trait>>
+chat_with_system()
+chat_with_history()
+chat_with_tools()
+supports_native_tools() bool
}
OpenAiCompatibleProvider --> AuthStyle
OpenAiCompatibleProvider ..|> Provider
Diagram: OpenAiCompatibleProvider Architecture
The OpenAiCompatibleProvider src/providers/compatible.rs:20-201 handles both standard and non-standard endpoint paths:
Standard Endpoint (most providers):
https://api.example.com → https://api.example.com/chat/completions
Full Path Endpoint (e.g., VolcEngine ARK):
https://api.example.com/api/coding/v3/chat/completions → (no modification)
Detection logic in chat_completions_url() src/providers/compatible.rs:128-146:
- If
base_urlalready ends with/chat/completions, use as-is - Otherwise, append
/chat/completions
The AuthStyle enum src/providers/compatible.rs:32-40 supports three header formats:
| Style | HTTP Header | Used By |
|---|---|---|
Bearer |
Authorization: Bearer {key} |
Most OpenAI-compatible services (default for custom:) |
XApiKey |
x-api-key: {key} |
Some Chinese providers |
Custom(String) |
{custom_header}: {key} |
Specialized endpoints |
Custom providers created with custom: prefix always use AuthStyle::Bearer src/providers/mod.rs:750-754.
Some providers expose both /chat/completions (tool calling) and /v1/responses (simple text) endpoints. The fallback mechanism src/providers/compatible.rs:822-831:
- Attempt POST to
/chat/completions - If
404 Not Foundandsupports_responses_fallback = true, retry with/v1/responses - Return combined error if both fail
GLM/Zhipu disables this fallback via new_no_responses_fallback() src/providers/compatible.rs:54-61 because it only supports chat completions.
Sources: src/providers/compatible.rs:20-201, src/providers/compatible.rs:128-146, src/providers/compatible.rs:32-40, src/providers/compatible.rs:822-831, src/providers/mod.rs:750-754
classDiagram
class AnthropicProvider {
+Option~String~ credential
+String base_url
+new(credential)
+with_base_url(credential, base_url) AnthropicProvider
-is_setup_token(token) bool
-apply_auth(request, credential) RequestBuilder
}
class Provider {
<<trait>>
+chat_with_system()
+chat()
+supports_native_tools() bool
}
AnthropicProvider ..|> Provider
note for AnthropicProvider "Supports OAuth setup tokens\n(sk-ant-oat01-) and regular\nAPI keys (sk-ant-)"
Diagram: AnthropicProvider with Custom Base URL
The AnthropicProvider::with_base_url() method src/providers/anthropic.rs:152-164 allows custom endpoints:
Default:
base_url = "https://api.anthropic.com"Custom (via anthropic-custom: prefix):
base_url = "https://proxy.example.com" // user-providedAll API calls append /v1/messages to the base URL src/providers/anthropic.rs:435.
Anthropic supports two credential types, auto-detected by is_setup_token() src/providers/anthropic.rs:166-168:
| Token Type | Prefix | Auth Method | Header |
|---|---|---|---|
| Setup Token (OAuth) | sk-ant-oat01- |
Bearer |
Authorization: Bearer {token} + anthropic-beta: oauth-2025-04-20
|
| Regular API Key | sk-ant- |
Custom | x-api-key: {key} |
The apply_auth() method src/providers/anthropic.rs:170-182 automatically selects the correct authentication scheme based on token prefix detection.
Sources: src/providers/anthropic.rs:152-164, src/providers/anthropic.rs:166-182, src/providers/anthropic.rs:435
Edit ~/.zeroclaw/config.toml:
OpenAI-compatible endpoint:
default_provider = "custom:https://api.mycompany.com"
api_key = "custom-api-key-123"
default_model = "my-model"Anthropic-compatible endpoint:
default_provider = "anthropic-custom:https://llm-proxy.corp.example.com"
api_key = "sk-internal-token"
default_model = "claude-3-5-sonnet"Local LLM server:
default_provider = "custom:http://localhost:8080"
default_model = "llama-3.1-8b"
api_key = "optional-key" # Optional for localhostCustom providers use the generic credential environment variables:
export ZEROCLAW_API_KEY="your-api-key"
# or
export API_KEY="your-api-key"
zeroclaw agent -m "test message"Specify provider and model at runtime:
# Interactive mode
zeroclaw agent --provider "custom:https://api.example.com" --model "gpt-4"
# Single message
zeroclaw agent --provider "anthropic-custom:https://proxy.local" \
--model "claude-3-opus" \
-m "Hello"Sources: docs/custom-providers.md:29-59, src/providers/mod.rs:537-545
flowchart LR
CustomProvider["Custom Provider\n(OpenAiCompatibleProvider\nor AnthropicProvider)"] --> Reliable["ReliableProvider Wrapper"]
Reliable --> Features["Resilience Features:\n• Exponential backoff\n• Max retries (configurable)\n• API key rotation\n• Model fallbacks\n• Rate limit handling"]
Features --> Return["Box<dyn Provider>"]
style Reliable fill:#f9f9f9
Diagram: Custom Provider Resilience Wrapping
Custom providers receive the same resilience features as built-in providers via create_resilient_provider() src/providers/mod.rs:796-840. Configuration in config.toml:
[reliability]
provider_retries = 3
provider_backoff_ms = 1000
fallback_providers = ["openrouter", "anthropic"] # Fallback if custom fails
model_fallbacks = { "my-model" = ["backup-model-1", "backup-model-2"] }The ReliableProvider wrapper src/providers/reliable.rs:182-653 provides:
- Retry Logic: Exponential backoff with configurable base delay src/providers/reliable.rs:241-248
- Error Classification: Distinguishes retryable (network, 5xx) from non-retryable (auth, 4xx) errors src/providers/reliable.rs:10-56
-
Rate Limit Handling: Parses
Retry-Afterheaders src/providers/reliable.rs:118-147 - Key Rotation: Cycles through multiple API keys on rate limit src/providers/reliable.rs:232-238
- Model Fallbacks: Tries alternative models if primary fails src/providers/reliable.rs:223-229
Sources: src/providers/mod.rs:796-840, src/providers/reliable.rs:182-653, src/providers/reliable.rs:10-56, src/providers/reliable.rs:118-147
Error: "Custom provider requires a valid URL"
Cause: URL failed reqwest::Url::parse() validation
Solution: Ensure URL includes scheme (http:// or https://) and valid domain:
# ✗ Invalid
default_provider = "custom:api.example.com"
# ✓ Valid
default_provider = "custom:https://api.example.com"Error: "API key not set" or 401 Unauthorized
Resolution steps:
-
Check credential presence src/providers/compatible.rs:780-785:
zeroclaw agent # Error message shows which env vars to set -
Verify credential resolution:
export ZEROCLAW_API_KEY="your-key" echo $ZEROCLAW_API_KEY # Confirm it's set
-
Test with explicit config:
api_key = "your-key" # Explicit beats env vars
-
Check auth header format: Custom providers use Bearer tokens. If your endpoint requires a different format, you may need to request a built-in provider addition or submit a PR to add custom auth style support.
Error: "Model not found" or 400 Bad Request
Diagnosis:
-
List available models (if endpoint supports
/models):curl -H "Authorization: Bearer $ZEROCLAW_API_KEY" \ https://your-api.com/v1/models -
Check model name format: Some providers require namespace prefixes (e.g.,
models/gemini-provsgemini-pro) -
Verify endpoint compatibility: Ensure the service actually implements OpenAI or Anthropic API format. Test with a known working model first:
zeroclaw agent --provider "custom:https://your-api.com" \ --model "gpt-3.5-turbo" \ -m "test"
Error: reqwest::Error with connection timeout or refused
Checklist:
-
Test raw connectivity:
curl -I https://your-api.com
-
Check proxy settings src/config/mod.rs. ZeroClaw respects
HTTP_PROXY/HTTPS_PROXYenvironment variables. -
Verify TLS certificate: Some corporate proxies use self-signed certificates. Check logs for TLS errors.
-
Local endpoints: For
http://localhost:*orhttp://127.0.0.1:*, ensure the local server is running:# Example: Ollama ollama serve # Then test zeroclaw agent --provider "custom:http://localhost:11434" --model "llama3"
Sources: src/providers/compatible.rs:780-834, docs/custom-providers.md:62-88
| Function | Location | Purpose |
|---|---|---|
create_provider_with_url |
src/providers/mod.rs:592-778 | Main factory for all providers including custom |
parse_custom_provider_url |
src/providers/mod.rs:549-570 | Validates and normalizes custom URLs |
resolve_provider_credential |
src/providers/mod.rs:465-547 | Hierarchical credential resolution |
create_resilient_provider |
src/providers/mod.rs:796-840 | Wraps providers with retry/fallback logic |
| Type | Location | Description |
|---|---|---|
OpenAiCompatibleProvider |
src/providers/compatible.rs:20-202 | Generic OpenAI-format provider |
AnthropicProvider |
src/providers/anthropic.rs:10-164 | Anthropic Messages API provider |
AuthStyle |
src/providers/compatible.rs:32-40 | Authentication header format enum |
ReliableProvider |
src/providers/reliable.rs:183-193 | Resilience wrapper with retries |
| Key | Type | Description | Default |
|---|---|---|---|
default_provider |
String | Provider name with optional custom: or anthropic-custom: prefix |
"openrouter" |
api_key |
String | API credential (or use env vars) | None |
default_model |
String | Model identifier | Provider-specific |
api_url |
String | Alternative to prefix syntax (applies only to primary provider) | None |
Sources: src/providers/mod.rs:592-778, src/providers/compatible.rs:20-202, src/providers/anthropic.rs:10-164, src/providers/reliable.rs:183-193