Three ways to add a provider ZeroClaw doesn't ship with:
- Use the
customslot. For any OpenAI-compatible endpoint not covered by an existing canonical slot. - Use the first-class local-server slots (
lmstudio,llamacpp,sglang,vllm,osaurus,litellm). Thin wrappers with sensible defaults. - Implement the
ModelProvidertrait in Rust. For anything that's not OpenAI-compatible.
If the service speaks OpenAI chat-completions, this is a config-only change. The custom slot requires uri (the family's endpoint enum has no default); reference it from an agent's model_provider.
This is the same OpenAiCompatibleModelProvider runtime impl used by groq, mistral, xai, and every other vendor with its own canonical slot in the catalog. The difference is which family slot you use: custom is the catch-all for endpoints not represented by a vendor slot.
ZeroClaw ships canonical slots for popular local-inference stacks. They're all OpenAI-compatible under the hood but with default uri values pre-applied so you can usually omit uri entirely.
Optional fields apply to any compat-slot family (including llamacpp). The
full set, derived from the schema:
{{#config-fields providers.models.custom}}
Controlling thinking mode varies by model family. think = false sets the top-level enable_thinking field in the request. Some models (e.g. Qwen3) read this flag from the Jinja template via chat_template_kwargs instead:
Other model families use different template variable names, check your model's chat template and set the appropriate key under chat_template_kwargs.
Slots lmstudio, osaurus, litellm follow the same pattern, see the catalog.
Bring-your-own-endpoint slots default to the OpenAI chat-completions wire. An endpoint that only speaks the OpenAI responses wire (some self-hosted vLLM / TGI deployments) needs an explicit wire_api = "responses" opt-in on the alias entry.
When set to "responses", the provider is built as an OpenAiResponsesModelProvider (full streaming tool calls over the responses protocol) instead of a chat-completions provider. Omit the field, or set "chat_completions", for the default wire.
wire_api is honored by the bring-your-own-endpoint families where the wire is operator-configurable: openai, llamacpp, and custom (plus the generic openai-compatible path). Branded vendor slots (groq, mistral, deepseek, …) have a fixed wire protocol and ignore the field, with one exception: opencode honors wire_api = "responses" because OpenCode Zen serves both wires. With no uri override, the OpenCode responses route targets https://opencode.ai/zen/v1/responses:
[providers.models.opencode.default]
model = "big-pickle"
wire_api = "responses"The setting governs both the primary agent path and delegate targets, so a delegate whose target alias declares wire_api = "responses" reaches the endpoint over the responses wire.
Regardless of approach:
zeroclaw config list # loads config; any validation failures print to stderr
zeroclaw models refresh --provider <type>.<alias> # list models the endpoint advertises
zeroclaw agent -a <alias> -m "hello" # smoke-test against the agent at `[agents.<alias>]`If the endpoint isn't OpenAI-compatible and isn't one of the local-server slots, you need code.
The trait lives in crates/zeroclaw-api/src/model_provider.rs:
#[async_trait]
pub trait ModelProvider: Send + Sync {
fn name(&self) -> &str;
fn supports_streaming(&self) -> bool { true }
fn supports_streaming_tool_events(&self) -> bool { false }
async fn chat(
&self,
messages: Vec<Message>,
tools: Vec<ToolSchema>,
options: ChatOptions,
) -> Pin<Box<dyn Stream<Item = Result<StreamEvent>> + Send>>;
}Implementation pattern:
- Define the typed config in
crates/zeroclaw-config/src/schema.rs:pub struct MyProviderModelProviderConfig { #[serde(flatten)] pub base: ModelProviderConfig, pub endpoint: MyProviderEndpoint, // family-specific fields } pub enum MyProviderEndpoint { Default } impl ModelEndpoint for MyProviderEndpoint { fn uri(&self) -> &'static str { match self { Self::Default => "https://my-provider.example.com/v1" } } }
- Add the slot to
for_each_model_provider_slot!incrates/zeroclaw-config/src/providers.rs. Every helper picks up the new slot automatically. - Add the runtime impl in
crates/zeroclaw-providers/src/myprovider.rs. TranslateVec<Message>to the wire format, stream the response, emitStreamEventvalues. - Wire the factory branch in
crates/zeroclaw-providers/src/lib.rs::create_provider_with_url_and_options. - Add a feature flag in
Cargo.tomlif the provider pulls heavy deps.
See anthropic.rs as a reference for a provider with a fully custom wire format. See compatible.rs for the SSE-streaming OpenAI-compat pattern.
- Verify the API key matches the endpoint (many vendors use key prefixes:
sk-,gsk_,sk-ant-). - Check that
uriincludes the scheme (http:///https://) and the/v1path if the endpoint expects it. - Endpoints behind a VPN or proxy? Confirm routing from the ZeroClaw host.
- List what the endpoint advertises:
curl -I $URI, does it respond?- Firewall, proxy, egress rules? VPS providers sometimes block outbound high ports.
- Vendor status page if it's a hosted service.
Some gateways (e.g. a LiteLLM proxy fronting claude-opus-4-7) return an error
when a temperature field is present at all. ZeroClaw honors the Option
contract: if you leave temperature unset in config, the field is omitted
from the request body entirely and the backend picks its own default. Only set
temperature explicitly when the endpoint accepts it.
- Overview: provider model and how per-agent dispatch works
- Configuration: full
[providers.*]schema, Azure typed config, regional and OAuth variants - Catalog: every canonical slot with a worked TOML example
- Developing → Plugin protocol: if a plugin works better than a first-class crate