Skip to content
Router One

Run Crush on Router One with a custom openai-compat provider

Crush is Charm's terminal coding agent: it runs file, shell, LSP and MCP tools on your machine and sends each model turn to the provider you configure. In crushrc, write the line provider add router-one --type openai-compat --base-url https://api.router.one/v1, then pin model large and model small to router-one IDs. That provider sends Crush's turns to Router One as POST /v1/chat/completions, each one a billed and traced request, while sessions, tools, permissions and context files stay in Crush. This guide targets Crush v0.96.1, released September 21, 2026, which prefers a Bash-based crushrc; the older crush.json still loads but is deprecated.

Install Crush v0.96.1 and export a dedicated key

Install Crush with Homebrew or npm, or with Scoop on Windows; as of September 23, 2026 all three shipped v0.96.1, while the winget package was still at 0.93.1. The v0.96.1 README also lists Arch, Nix and FreeBSD packages. crush --version should print v0.96.1. Create a Router One key for Crush with a maxSpend cap and export it in the shell that starts Crush: crushrc runs when Crush launches, so it sees only variables that already exist in that environment. ROUTER_ONE_API_KEY is this guide's own name; Crush reads it only because crushrc references it. crush dirs prints the config directory, where crushrc belongs, followed by the data directory Crush writes its state to.

terminal
brew install charmbracelet/tap/crush      # or: npm install -g @charmland/crush
crush --version                           # crush version v0.96.1
export ROUTER_ONE_API_KEY="sk-your-router-one-key"
crush dirs                                # config directory first, then data directory
# Windows (Scoop; the winget package may lag behind):
#   scoop bucket add charm https://github.com/charmbracelet/scoop-bucket.git
#   scoop install crush
# PowerShell: $env:ROUTER_ONE_API_KEY = "sk-your-router-one-key"

Configure Crush to use the Router One base URL

Save the block as crushrc in the config directory: ~/.config/crush/crushrc on macOS and Linux ($XDG_CONFIG_HOME/crush/crushrc when that variable is set) and %USERPROFILE%\.config\crush\crushrc on Windows. crushrc is Bash that Crush's built-in interpreter runs at startup; provider add, model add, model large and model small are Crush builtins, not system commands. router-one is a local provider ID. --type openai-compat selects Crush's Chat Completions client, and --base-url is the /v1 base URL without /chat/completions. The ${ROUTER_ONE_API_KEY:?…} form stops Crush with a config error when the variable is missing. A plain "$ROUTER_ONE_API_KEY" would expand to an empty key, and Crush's OpenAI client would then fall back to OPENAI_API_KEY if your environment exports one and send that key to Router One. model add takes <provider>/<id> and splits only at the first slash, so router-one/anthropic/claude-sonnet-5 registers the catalog ID anthropic/claude-sonnet-5 and sends it unchanged. Replace both IDs with current chat models from /models whose detail pages list tool calling: the large slot runs the coding agent, and the small slot writes session titles and runs a fetch sub-agent that calls tools too. The block leaves --context-window out because its value comes from each model's page on /models, which shows it abbreviated and rounded (for example 200K or 1.05M). Before long sessions, append --context-window to each model add line with that value written as a whole number and rounded down (200K → 200000, 1.05M → 1000000); a value such as 200K stops Crush at startup. A .crushrc or crushrc in the project overrides the global file. Both are trusted code, so keep the key in the environment rather than in a file you commit.

~/.config/crush/crushrc
# ~/.config/crush/crushrc   (Windows: %USERPROFILE%\.config\crush\crushrc)
# Router One as a custom OpenAI-compatible provider -> POST /v1/chat/completions
provider add router-one \
  --name "Router One" \
  --type openai-compat \
  --base-url "https://api.router.one/v1" \
  --api-key "${ROUTER_ONE_API_KEY:?set ROUTER_ONE_API_KEY}"

# <provider>/<id>: Crush splits at the first slash, so the catalog ID keeps its own slash
# Append --context-window <model-page value as a whole number, e.g. 200000> to each line;
# without it Crush never auto-summarizes
model add router-one/anthropic/claude-sonnet-5 --name "Claude Sonnet 5 (Router One)"
model add router-one/anthropic/claude-haiku-4.5 --name "Claude Haiku 4.5 (Router One)"

# Pin both slots so titles and the fetch sub-agent bill this key too
model large router-one/anthropic/claude-sonnet-5
model small router-one/anthropic/claude-haiku-4.5

Test once with crush run, and keep the router-one/ prefix in -m

Piped as below, crush models prints every model Crush knows as provider/model lines (run directly in a terminal, it draws a tree grouped by provider); filtering for router-one/ confirms that crushrc loaded. crush run sends a real, billed prompt. Besides the answer, Crush generates a session title, so one prompt normally appears as two POST /v1/chat/completions requests in Dashboard → Logs. In a local mock test of v0.96.1, the title request used the small model with a 40-token output limit and no tools; the main request used the large model with stream: true, stream_options.include_usage and Crush's tool definitions. That main request carries Crush's full system prompt and its tool definitions (26 in the test, about 49 KB of JSON), so even this greeting bills thousands of input tokens on the large model. When you pass -m, keep the router-one/ prefix and pass --small-model as well. crush run reads the first segment as a provider only when a provider with that ID exists, so -m openai/gpt-5.5 goes to the built-in OpenAI provider whenever that provider is configured. For a custom provider such as router-one, -m without --small-model also moves the title request to the large model for that run. In the TUI, ctrl+l opens the model picker. A choice made there is saved to the data file, ~/.local/share/crush/crush.json or %LOCALAPPDATA%\crush\crush.json on Windows, which loads after the global crushrc and overrides its model lines. Crush also writes that file itself: when a model large or model small line names an ID that is not registered on the provider, it falls back to a default model without stopping and saves the fallback there. If an edit to crushrc's model lines seems to be ignored, delete the models entry from the data file.

terminal
crush models | grep '^router-one/'
crush run "Reply with one short greeting."
# One-off model choice: prefix both slots with router-one/
crush run -m router-one/anthropic/claude-sonnet-5 \
  --small-model router-one/anthropic/claude-haiku-4.5 "Reply with one short greeting."
crush            # interactive; ctrl+l opens the model picker

Keep auxiliary calls on the same key

Crush fills two model slots. The large model runs the coder, plan and task agents and writes context summaries. The small model writes session titles and runs agentic_fetch, a sub-agent with its own web fetch, web search, Sourcegraph, glob, grep and view tools. The title request is capped at 40 output tokens unless the model add line sets --can-reason; if it fails or stops at that cap, Crush repeats it on the large model, so a small model that spends output on reasoning can add a second billed title request to each new session. When model small is not set, Crush chooses for you: with only custom providers it reuses the large model, but a configured built-in provider takes precedence. In a v0.96.1 mock test with OPENAI_API_KEY exported and only model large pinned to router-one, the title request went to the built-in OpenAI provider and its key, not to Router One. Pin model small to a router-one ID, as the config above does. If Router One should be the only provider, also add option default-providers false; Crush then ignores every built-in provider, including any you use for other work.

The provider type decides the request path

Crush picks its client from --type; changing only --base-url never translates between protocols. Crush's README recommends openai-compat for OpenAI-compatible APIs other than OpenAI's own, which is what this guide uses; --type openai selects Crush's OpenAI client instead, and a custom provider without --type is treated as openai-compat. To use a native protocol as well, add a second provider with its own ID, type and model list. The README's Anthropic-compatible example ends its base URL in /v1, which with this client doubled the path to /v1/v1/messages. The last column shows the request the v0.96.1 mock test received, followed by notes on how Router One serves it:

--type--base-urlRequest Crush sent in the local test, and Router One notes
openai-compat (this guide)https://api.router.one/v1POST /v1/chat/completions for every model ID, with Authorization: Bearer; the endpoint that serves every current chat model
openaihttps://api.router.one/v1POST /v1/responses when the ID contains gpt- plus a generation number of 4 or higher, such as openai/gpt-5.5, with store: false and reasoning.encrypted_content requested; other IDs stay on POST /v1/chat/completions. Confirm that the model page lists /v1/responses
anthropichttps://api.router.one (host root, no /v1)POST /v1/messages with x-api-key and anthropic-version headers; Claude-family and DeepSeek IDs only. A base URL ending in /v1 produced /v1/v1/messages

Context window, retries and cost records

Any numbers you add to a model add line (--context-window, --default-max-tokens, --price-*) are Crush-side settings; Router One does not supply them. Without --context-window a model's window is 0, and Crush then never auto-summarizes that session, so the history resent with each request keeps growing and can eventually exceed what the model accepts. Convert the model page's context window to a whole number for --context-window, rounding down (200K → 200000, 1.05M → 1000000); a lower value only makes Crush summarize a little earlier. Crush summarizes when 20,000 tokens remain for windows above 200,000, or when 20% remains for windows of 200,000 or less; each summary is one more request on the large model, and option auto-summarize false turns this off. --default-max-tokens becomes the output limit on main requests; without it, the Chat Completions requests in the mock test carried no max_tokens field. On 408, 409, 429, 5xx and transport errors, Crush retries a failed call up to three times, waiting about 5, 10 and 20 seconds (or the Retry-After delay when the server asks for less than 60 seconds), so one step can appear as four requests in Logs. 400 and 402 errors are returned immediately. v0.96.0 raised the default request timeout to 120 seconds (the v0.96.1 config reference still says 60), which acts as an inactivity limit for streams; set option request-timeout for slower models. A stream Crush cancels this way appears in Logs as HTTP 499 client_cancelled and is billed only for the usage reported for it. Crush's session cost comes from the --price-* flags and stays at zero without them, so Logs remains the billing record. crush --debug writes each request URL and JSON body to .crush/logs/crush.log in the project, readable with crush logs. That includes prompts and code, so delete the log after debugging.

Which model ID should Crush send?

Copy the exact model ID from /models, preserving case, hyphens, and version suffixes; do not substitute a display name. Open its detail page and match the supported API endpoints, context window, and capabilities such as tool calling to the provider and features selected in Crush. A catalog listing does not mean the client can use every feature of that model. Give each client or application a dedicated API key with a maxSpend cap.

Which API protocol is Crush using?

OpenAI-compatible describes an interface format; it does not make Chat Completions (/v1/chat/completions), Responses (/v1/responses), and Anthropic Messages (/v1/messages) interchangeable. Check the installed client version, provider configuration, and actual request path against the model detail page and API compatibility fact sheet. A successful plain-text chat does not establish support for hosted tools, conversation state, or file-editing features.

Verify the Crush call in your request trace

Send a simple text request from Crush, then match its trace in Dashboard → Logs by time, model, and request_id: tokens, cost, latency, and status. Next, test streaming, tool calls, and multi-turn history separately. For failures, retain the actual request path, full error message, and request_id. If there is no matching log, check client configuration and connectivity before attributing the error to the gateway or upstream.

FAQ

Crush starts, but every request returns 401. What should I check first?

Check whether the key reached Crush. crushrc expands variables when Crush starts. With a plain "$ROUTER_ONE_API_KEY" and no such variable in that environment, v0.96.1 still starts with an empty key, and its OpenAI client falls back to OPENAI_API_KEY. In a local mock test the requests carried no Authorization header when OPENAI_API_KEY was unset, and carried the OpenAI key as a Bearer token when it was exported, so Router One rejects it with 401 after that key has already left your machine. The legacy crush.json behaves the same way. The ${ROUTER_ONE_API_KEY:?set ROUTER_ONE_API_KEY} form used above makes Crush stop at startup instead, with a config error that ends in "executing shell config …/crushrc: exit status 1"; the hint after :? is not printed. Export the variable in the shell that launches Crush and restart it. If the header is present, confirm that the key is active and was copied completely, and that router-one served the request: a model chosen from a built-in provider uses that provider's key.

I already have a crush.json. Do I need to switch to crushrc?

No. v0.96.1 still loads crush.json from the global config directory and .crush.json or crush.json from project directories, although the format is deprecated and new options are added only to crushrc. The equivalent entry is providers.router-one with type openai-compat, base_url https://api.router.one/v1, api_key "$ROUTER_ONE_API_KEY" (both the $VAR and ${VAR} forms expanded in the mock test) and a models array whose objects carry id, name and, optionally, context_window and default_max_tokens. crush.json has no missing-variable guard: even ${ROUTER_ONE_API_KEY:?…} there only makes v0.96.1 log a warning and continue with an empty key, so export the variable before launching Crush (see the 401 question above). The top-level models object takes large and small, each with provider router-one and the catalog ID as model. Add "$schema": "https://charm.land/crush.json" to reference the published schema. When one directory holds both formats they merge, crushrc wins on conflicts, and Crush logs a warning if both set the same top-level keys.

Should I name the provider openai, or use --type openai for GPT models?

Keep a separate ID such as router-one. openai is the ID of Crush's built-in OpenAI provider, so provider add openai only overrides that provider's URL and key. Its built-in list of bare IDs such as gpt-5.5 and its default models stay in place, and those IDs would reach Router One without the openai/ prefix the catalog uses. --type openai is a separate choice: it sends IDs such as openai/gpt-5.5 to /v1/responses with store: false, a reasoning summary and encrypted reasoning content requested, while other IDs stay on Chat Completions. Router One serves /v1/responses natively for the currently listed GPT-family IDs, but confirm on one real request that those Responses fields are accepted before relying on them. openai-compat keeps every model on Chat Completions, the path used throughout this guide.

Why does the model picker show only the models I added?

Because the provider lists its models explicitly. With --discover-models true on provider add, Crush also requests GET /v1/models from Router One each time it loads the config, with a 3-second limit, and appends the returned IDs after yours. Discovered entries carry only the ID, with no context window, output limit or price, and the list includes image-generation IDs that cannot serve chat turns. A router-one provider with no models at all triggers discovery automatically, and Crush drops the provider if that discovery fails. Explicit model add lines are more predictable for the slots you actually use.

Which models can Crush use through the gateway?

Choose a current catalog model that supports both the endpoint and the features Crush uses. Check /models and the model detail page for the exact ID, current rates, and capabilities; a family name such as GPT or Claude is not a compatibility guarantee. A client that lists a model has only read the ID, from its own configuration or from GET /v1/models; verify an actual request too.

Models are listed, but requests fail with 400 or 404. What should I check?

Record the actual request path and error message, then check the exact model ID. A 400 can indicate invalid parameters, unsupported tools, or a model/endpoint mismatch; a 404 can indicate an incorrect path or missing resource, so it does not by itself establish that a model was retired. If the error says must be called via, use the named endpoint or select a model supported on the current endpoint. Do not add or remove /v1 or /chat/completions across all clients indiscriminately.

Does this work from Mainland China?

Yes. The gateway is reachable from Mainland China without a VPN, and the configuration is identical to the global setup.

How do I debug a 401/402/403/429?

Match the request and error message in Dashboard → Logs. For 401, check whether the key was sent and is valid; for 402, check wallet balance and maxSpend; for 403, check key permissions and access restrictions. For 429, distinguish request/token limits from upstream throttling using the error details. Keep the request_id and follow the error-codes reference.