crab0.4.0
crab/docs/Providers & models

crab speaks one wire protocol: the OpenAI chat-completions API. A provider is a profile with a base URL, a model id, an optional key, and a context window. That is the whole abstraction.

The config file

~/.crab/config.toml. A missing file is fine — it means "environment variables only". default_provider names the profile used when you do not pass --provider.

default_provider = "deepseek"

[providers.deepseek]
base_url       = "https://api.deepseek.com/v1"
model          = "deepseek-chat"
api_key        = "sk-..."          # optional if CRAB_API_KEY / OPENAI_API_KEY is set
context_window = 128000

[providers.local]
base_url       = "http://localhost:8080/v1"
model          = "qwen"

auto_compact_threshold = 0.8       # auto-compact above 80% of the window; 0 disables

Provider fields

KeyTypeNotes
base_urlstringOpenAI-compatible base URL. Required.
modelstringModel name sent to the API. Required.
api_keystring?Falls back to CRAB_API_KEY / OPENAI_API_KEY. Local endpoints may leave it empty; anything else without a key is an error.
context_windowinteger?Shown in the status line; enables the out-of-window warning and drives compaction.

Resolution order

The --provider <name> flag wins, then default_provider, then openai. Within the chosen profile, environment variables beat the file, field by field:

EnvironmentOverrides
CRAB_API_KEY / OPENAI_API_KEYapi_key
CRAB_BASE_URL / OPENAI_BASE_URLbase_url
CRAB_MODELmodel
CRAB_CONTEXT_WINDOWcontext_window

So export CRAB_API_KEY=… with no config file at all is a working setup: crab uses https://api.openai.com/v1 and gpt-4o-mini.

Switching

crab --provider deepseek           # for one run
crab --provider mock              # canned responses, for tests and demos

Inside a session, two slash commands do the same thing without losing history:

/model <name>
Switch to a named profile. The next turn uses it; the transcript is unchanged.
/models
Pick from the profiles in your config with ↑↓ / Enter.

Because the transcript is provider-agnostic, switching mid-conversation is normal — start on a cheap model, move to a stronger one when the task gets hard. The ledger records which model each turn used, so the cost report stays honest.

Choosing a context window

context_window is crab's belief about how many tokens the model accepts. It is used for the occupancy display, the "running out of window" warning, and the auto-compaction threshold — not for validation. When in doubt, understate it: an early compaction costs you a summary; a late one costs you a failed turn.

Advertised context is not usable context. Many models reserve room for output. If your provider documents a maximum output length, subtracting it from the advertised window is a better number than the advertised window itself.

Local models

crab does not run inference. Point base_url at a server that speaks the same API:

[providers.local]
base_url       = "http://127.0.0.1:8080/v1"
model          = "qwen3-coder-30b"
context_window = 131072

That works with llama.cpp's server, vLLM, and Ollama's OpenAI-compatible endpoint. Local endpoints are allowed an empty key. Two things to check when a local run misbehaves: the model must support tool calling, and its chat template must survive the tool-result round trip. If it ignores tool definitions you get a chatty agent that never edits anything — a model problem, not a harness one.

Prompt caching

crab does not manage the cache; it reports it. Providers that cache prefix tokens — DeepSeek and Kimi among them — return cache_read and cache_write counts, and crab records them verbatim in a four-way split. Two practical consequences:

  • A stable prefix is cheaper than a clever one. Prompt text that changes every turn defeats the cache.
  • The first turn of a session writes cache; later turns read it. A short session can be dominated by cache writes.

See the pricing model for how those counts become money, and ~/.crab/pricing.toml for how to override the rates.

MCP servers

External tools come over the Model Context Protocol. Add servers under [mcp.<name>] in the same file:

[mcp.brave]
command = "npx"
args    = ["-y", "@modelcontextprotocol/server-brave-search"]
enabled = true
env     = { BRAVE_API_KEY = "..." }
FieldTypeNotes
commandstringProgram to spawn. Required.
argsstring[]Arguments. Default empty.
enabledbool?Omitted means enabled. false skips the server entirely.
envtableExtra variables for the child, layered on top of the inherited environment — so PATH still works.

Putting a credential in env rather than in args also keeps it out of ps output and out of the permission prompt.

Tool naming

Every tool a server exposes is registered as <server>__<tool> — two double underscores. Two servers may both expose search, and the model needs to tell them apart:

[mcp.brave]  →  brave__brave_web_search
[mcp.docs]   →  docs__lookup

# two servers, same tool name, distinguishable
[mcp.alpha]  →  alpha__search
[mcp.beta]   →  beta__search

Several tools from one server share a single child process.

Startup is best-effort

A command that fails to spawn, or whose tools/list fails, prints a warning on stderr and is skipped. One broken MCP server never prevents crab from starting.

Node servers behind a proxy

npx-style servers inherit your shell environment, including HTTP_PROXY / HTTPS_PROXY. But Node's fetch (undici) ignores those variables by default, so a Node server behind a corporate proxy may fail to reach the network. The fix belongs in the server's env:

[mcp.brave]
command = "npx"
args    = ["-y", "@modelcontextprotocol/server-brave-search"]
env     = { BRAVE_API_KEY = "...", NODE_OPTIONS = "--use-env-proxy" }

--use-env-proxy (equivalently NODE_USE_ENV_PROXY=1) makes Node's built-in http/https and fetch honour HTTP_PROXY / HTTPS_PROXY / NO_PROXY. It is opt-in and needs a recent Node (the fetch path is 22.21.0 / 24.0.0+).

Skills

crab discovers Agent Skills — directories containing a SKILL.md with name / description frontmatter — in two places:

Project
.agents/skills/ in the current directory or any parent, up to and including the git repo root.
User
~/.agents/skills/.

Discovered skills appear in the system prompt by name, description and path. The body stays on disk, and the model is told to read the SKILL.md with the read tool when a task matches — progressive disclosure without paying for every skill on every turn. On a name collision the first discovery wins: project beats user, nearer beats farther.

/skill:<name> [args] is the explicit path: it injects the whole SKILL.md body as a user message with the args appended. Skills may bundle extra files (references/, scripts/, …) relative to their directory.

esc