Everything lives in one file: config.json (copied from
config.example.json on first run). To add a backend you edit two sections:
models— what appears in Claude Code's/modelpicker.routes— where each of those ids actually goes.
Two rules you must follow:
- Every
idinmodelsmust start withclaudeoranthropic— Claude Code drops everything else from/model.- The
idinmodelsmust exactly equal the key inroutes. Otherwise the model won't appear or won't route.Run
python scripts/doctor.pyand it checks both for you.
config.json is gitignored, so you can put a key inline:
"auth": "Bearer sk-...your-real-key..."…or keep it out of the file with ${ENV} expansion:
"auth": "Bearer ${MIMO_API_KEY}"${VAR} is read from your environment. The launchers also load an optional
gitignored ultracode.env in the repo root, so you can keep keys there:
MIMO_API_KEY=...
OPENROUTER_API_KEY=...
MiMo, DeepSeek, StepFun, Ollama Cloud, OpenRouter, OpenAI, Together, a local llama.cpp / LM Studio server, etc. Tool calls are translated both ways.
"claude-openrouter": {
"type": "openai_compat",
"upstream": "https://openrouter.ai/api/v1",
"model": "meta-llama/llama-3.3-70b-instruct",
"auth": "Bearer ${OPENROUTER_API_KEY}"
}Requesty is another OpenAI-compatible gateway — same
shape, just a different upstream and provider/model naming
(docs, keys at
app.requesty.ai/api-keys):
"claude-requesty": {
"type": "openai_compat",
"upstream": "https://router.requesty.ai/v1",
"model": "openai/gpt-4o-mini",
"auth": "Bearer ${REQUESTY_API_KEY}"
}upstreamis the OpenAI base URL exactly as the provider documents it (usually ends in/v1). The proxy appends/chat/completions.modelis the backend's real model id, not theclaude-…alias.- Optional:
headers(a dict, values support${VARS}),max_output_tokens(completion cap, default 8192 — raise it if a backend supports longer output), andbody(a dict merged into every request body, values support${VARS}) — for provider-specific flags like MiniMax‑M3'sreasoning_split(see below).
A local server is the same, with a usually-ignored key:
"claude-local": {
"type": "openai_compat",
"upstream": "http://127.0.0.1:11434/v1",
"model": "your-local-model",
"auth": "Bearer local"
}MiniMax‑M3 is OpenAI‑compatible (it's the openai_compat type), but it has one
gotcha worth calling out:
"claude-minimax-m3": {
"type": "openai_compat",
"upstream": "https://api.minimax.io/v1",
"model": "MiniMax-M3",
"auth": "Bearer ${MINIMAX_API_KEY}",
"max_output_tokens": 64000,
"body": { "reasoning_split": true }
}- Get a key at platform.minimax.io → put it
inline or set
MINIMAX_API_KEY(env orultracode.env). "body": { "reasoning_split": true }is the important part. M3 is a reasoning model: by default it streams its chain‑of‑thought inline as<think>…</think>right inside the answer, which clutters Claude Code's output and confuses tool parsing. Withreasoning_spliton, the thinking is returned in a separatereasoning_contentfield, so the visible answer stays clean. Leave it out and you'll see raw<think>blocks in replies.modelisMiniMax-M3(capitalized exactly like that).max_output_tokenscan go up to 64000; M3's context is ~1M tokens.- The
bodydict is generic — anyopenai_compatbackend can use it to pass provider‑specific request params (values support${VARS}).
The shipped config.example.json already includes this entry — just add your key.
"claude-opencode": {
"type": "openai_compat",
"upstream": "https://opencode.ai/zen/go/v1",
"model": "deepseek-v4-pro",
"auth": "Bearer ${OPENCODE_API_KEY}",
"headers": { "User-Agent": "openclaw/2026.4.20" }
}- It's an OpenAI-compatible API, not Anthropic. The base URL ends in
/zen/go/v1; the proxy appends/chat/completions. - Model ids are bare —
deepseek-v4-pro,deepseek-v4-flash,kimi-k2.6,glm-5.1,minimax-m3, … — not theopencode-go/-prefixed ids theopencodeCLI prints (that prefix is the CLI's provider namespace, not the API id). A wrong id returns401 {"type":"ModelError","message":"Model … is not supported"}. - A
User-Agentheader is required: the endpoint is behind Cloudflare, which rejects the default client UA with403 error code: 1010. https://opencode.ai/zen/v1(no/go) is the separate pay-as-you-go endpoint;…/zen/go/v1is the subscription. An empty PAYG balance shows as401 {"type":"CreditsError","message":"Insufficient balance"}.- DeepSeek V4 models are reasoning models — their chain-of-thought returns as
reasoning_content, which the proxy keeps out of the visible answer.
Omit type (or set "anthropic"). With no auth/upstream it's just real
Claude with the UltraCode envelope. You can also point at another
Anthropic-shaped gateway and add headers:
"claude-gateway": {
"upstream": "https://your-anthropic-gateway.example.com",
"model": "claude-sonnet-4-5",
"auth": "Bearer ${GATEWAY_API_KEY}"
}"claude-gpt-5.5-codex": { "type": "codex_oauth", "model": "gpt-5.5" }Run codex login once (creates ~/.codex/auth.json). No auth/upstream
needed. Optional env knobs: UC_CODEX_EFFORT, UC_CODEX_SERVICE_TIER,
CODEX_HOME.
"claude-composer": { "type": "cursor_agent", "model": "composer-2.5" }Needs the cursor-agent CLI and cursor-agent login. It's an autonomous agent,
not a plain endpoint, so we run it in read-only "ask" mode and bridge tool calls
as text markers — great for reasoning/answers, best-effort for tool-calling (the
model may not match your tool's exact argument names).
Live-tested: plain answers and the tool bridge both work (~4–7s per turn). Knobs:
CURSOR_AGENT_TIMEOUT (default 240s) and CURSOR_AGENT_WORKSPACE.
Behind an intercepting HTTP(S) proxy?
cursor-agenttalks to Cursor's cloud itself; a TLS-intercepting proxy can make it hang/time out. SetCURSOR_AGENT_NO_PROXY=1to stripHTTP(S)_PROXY/ALL_PROXYfrom the cursor-agent child process.
config.example.json includes ready-to-use entries — keep the ones you have a
plan/key for and delete the rest:
| Picker label | id | type |
backend |
|---|---|---|---|
| GPT-5.5 (Codex OAuth) | claude-gpt-5.5-codex |
codex_oauth |
codex login |
| MiniMax-M3 | claude-minimax-m3 |
openai_compat |
MiniMax (reasoning_split) |
| MiMo v2.5 Pro | claude-mimo |
openai_compat |
Xiaomi MiMo |
| DeepSeek V4 Pro/Flash | claude-deepseek-v4-* |
openai_compat |
DeepSeek |
| Step Flash | claude-step-flash |
openai_compat |
StepFun |
| Ollama Cloud | claude-ollama-cloud |
openai_compat |
Ollama Cloud |
| DeepSeek V4 Pro (OpenCode Go) | claude-opencode |
openai_compat |
OpenCode Zen (Go subscription) |
| Llama 3.3 70B (OpenRouter) | claude-openrouter |
openai_compat |
OpenRouter |
| GPT-4o mini (Requesty) | claude-requesty |
openai_compat |
Requesty |
| Local model | claude-local |
openai_compat |
local server |
| Composer 2.5 (experimental) | claude-composer |
cursor_agent |
cursor-agent |
| Auto (smart routing) | claude-auto |
auto |
picks among your backends per task |
Adding a model to the /model menu (above) is independent of the
Auto Router. To let the router choose a model automatically,
also list it under router.candidates with a relative cost, an
supports_images flag, and an honest capability card:
"router": {
"enabled": true,
"classifier": "claude-mimo",
"candidates": [
{ "id": "claude-minimax-m3", "cost": 0.3, "supports_images": false,
"card": "cheap, fast; single-file edits, codegen, simple refactors; weak on big refactors/debugging" },
{ "id": "claude-mimo", "cost": 1.0, "supports_images": false,
"card": "cheap generalist; standard infra/CRUD, data processing, moderate multi-file edits" }
]
}The candidate id must match a route. Candidates without a route are skipped, so
the router keeps working with whatever subset you keep. Full reference:
AUTO_ROUTER.md.
After editing, validate and launch:
python scripts/doctor.py
windows\Start-UltraCode.ps1 # or ./bin/ultracode on mac/linux

{ "models": [ { "id": "claude-mimo", "display_name": "MiMo v2.5 Pro" } ], "routes": { "claude-mimo": { "type": "openai_compat", "upstream": "https://token-plan-sgp.xiaomimimo.com/v1", "model": "mimo-v2.5-pro", "auth": "Bearer ${MIMO_API_KEY}" } } }