opencode setup: custom provider, model ids and output limits
Configure opencode for uttapen in opencode.json: an OpenAI-compatible provider, exact model ids, limit.output to keep holds small, a cheap title model.
Updated: 22 Sep 2026
opencode is a terminal coding agent that works with any Chat Completions endpoint. You add uttapen as a custom provider: the base URL https://api.uttapen.ir/v1, your sk-up-… key, and the models you want in the picker. This page was checked against opencode 1.18.31, and the request bodies quoted below were captured from that version against a test server.
When opencode doesn't know a model's output limit, it sends max_tokens: 32000 on every request, and it writes the session title with your main model. So each message is two concurrent requests, each reserving the cost of 32,000 output tokens. On a model like Claude Sonnet that alone can trigger 402 on a healthy balance. The limit.output and small_model settings below fix it.
Before you start
- A key from Dashboard → API keys. Give opencode its own key with a monthly cap so its spend shows up separately.
- Some balance in the wallet.
- The key in an environment variable rather than in a file:
export UTTAPEN_API_KEY="sk-up-..."
opencode.json
Put this in your project root as opencode.json, or in ~/.config/opencode/opencode.json to use it everywhere. opencode merges both files and accepts JSONC (comments allowed).
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"uttapen": {
"npm": "@ai-sdk/openai-compatible",
"name": "uttapen",
"options": {
"baseURL": "https://api.uttapen.ir/v1",
"apiKey": "{env:UTTAPEN_API_KEY}"
},
"models": {
"anthropic/claude-sonnet-5": {
"name": "Claude Sonnet 5",
"limit": { "context": 200000, "output": 8192 }
},
"qwen/qwen3-coder": {
"name": "Qwen3 Coder",
"limit": { "context": 262144, "output": 8192 }
},
"deepseek/deepseek-v3.2": {
"name": "DeepSeek V3.2",
"limit": { "context": 163840, "output": 8192 }
},
"openai/gpt-5-nano": {
"name": "GPT-5 nano",
"limit": { "context": 400000, "output": 1024 }
}
}
}
},
"model": "uttapen/anthropic/claude-sonnet-5",
"small_model": "uttapen/openai/gpt-5-nano"
}
What each part does:
npmmust be@ai-sdk/openai-compatible, which only calls/v1/chat/completions. Don't use@ai-sdk/openai: it talks to/v1/responses, which uttapen does not serve, so you get404.uttapenis the provider id inside opencode. Any name works, as long asmodelandsmall_modeluse the same one.options.apiKeywith{env:UTTAPEN_API_KEY}reads the key from the environment. To let opencode store the key instead, drop that line, run/connectin opencode, choose Other, enter the provider iduttapenand paste the key.- The keys under
modelsmust be exact uttapen model ids; opencode sends that string as the request'smodel. limit.outputbecomes themax_tokensopencode sends, capped at 32,000. Leave it out and 32,000 is sent.- Always set
limit.context. Without it opencode never auto-compacts the conversation, so every request carries a bigger (and pricier) prompt. A number below the model's real context makes opencode compact earlier: Claude Sonnet 5 accepts a million tokens, but 200,000 is plenty for daily work and keeps prompts cheaper. small_modelis what opencode uses for light jobs such as session titles. For a custom provider opencode can't pick a cheap model by itself, so without this line titles go to your main model with the same large reservation.
opencode does not read /v1/models for a custom provider; only the models listed under models show up in the picker.
Picking a model
Copy ids from the model list or the model's own page, which also shows its context window and output ceiling (the same numbers are in GET https://api.uttapen.ir/v1/models as context_length and top_provider.max_completion_tokens). opencode names models providerID/modelID, so anthropic/claude-sonnet-5 under the uttapen provider is uttapen/anthropic/claude-sonnet-5. opencode splits on the first / only and keeps the rest intact.
Common choices for coding:
| Role | Model id |
|---|---|
| Strong main model | anthropic/claude-sonnet-5, openai/gpt-5 |
| Budget main model | qwen/qwen3-coder, deepseek/deepseek-v3.2, openai/gpt-5-mini |
small_model (titles) | openai/gpt-5-nano, google/gemini-2.5-flash-lite |
List what you configured, then switch between models with /models inside opencode:
opencode models uttapen
Keeping cost under control
Before a request goes out, uttapen reserves the cost of the largest possible output from your balance, then charges only real usage when the reply ends (pricing). A large max_tokens doesn't cost more, but it locks more of your balance while the request runs — and opencode runs several requests at once, so the holds add up.
- Set
limit.outputto about8192for main models. That makes the output part of each hold roughly a quarter of the 32,000 default and is enough for code edits. Reasoning models, which think before answering, are safer at16000. - Point
small_modelat a cheap model withlimit.outputaround1024. - If you don't want to set a number per model,
OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAXis a global ceiling: opencode sends the smaller of it andlimit.output. As the name says, it is experimental. - Give the opencode key a monthly cap, and an allowed-models list if you want one (API keys).
With the config above, opencode 1.18.31 sent the title request to openai/gpt-5-nano with max_tokens: 1024 and the real turn to anthropic/claude-sonnet-5 with max_tokens: 8192. Without those settings both requests went to Claude Sonnet 5 with max_tokens: 32000.
Checking that it works
Test the key on its own first:
curl https://api.uttapen.ir/v1/chat/completions \
-H "Authorization: Bearer $UTTAPEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"openai/gpt-5-nano","max_tokens":20,"messages":[{"role":"user","content":"ping"}]}'
Then run opencode without the full-screen UI:
opencode run -m uttapen/openai/gpt-5-nano "say hi"
If you get an answer, Dashboard → Usage should show two small requests: the title and the message.
Troubleshooting
| Error | Cause in opencode | Fix |
|---|---|---|
401 invalid_api_key | UTTAPEN_API_KEY isn't set in the shell that launched opencode, or the key was revoked | Run echo $UTTAPEN_API_KEY in that terminal, or store the key again with /connect |
404 model_not_found | A key under models differs from the real id, e.g. claude-sonnet-5 without anthropic/, or claude-sonnet-4-5 with a dash instead of a dot | Copy the id from /en/models (the 404 also lists the closest ids it guessed, in uttapen.suggestions) |
404 not_found | npm is @ai-sdk/openai, so the request went to /v1/responses | Use @ai-sdk/openai-compatible |
402 insufficient_balance | The balance can't cover even 1,024 output tokens for this request; concurrent holds add up | Top up; set limit.output and small_model as above |
429 | The key's per-minute or monthly cap is reached, or the model is briefly rate-limited | Read the error code (rate limits); raise the key's rate_limit_rpm |
| Reply stops mid-sentence | finish_reason: "length": limit.output is low, or uttapen lowered the ceiling | See below |
| opencode just sits there — no error, no reply | Stuck in init, before it ever reaches uttapen | See below |
If opencode never gets past startup (stuck at init). Check ~/.local/share/opencode/log/opencode.log; if the last line of the run is message=init and nothing follows, it hasn't reached the uttapen gateway yet. This isn't caused by uttapen: on startup opencode makes a few background network calls of its own — fetching the model list from models.opencode.ai (unless OPENCODE_DISABLE_MODELS_FETCH=1) and checking for a new release (unless OPENCODE_DISABLE_AUTOUPDATE=1). That host, opencode.ai itself, and even registry.npmjs.org all sit behind Cloudflare, and on an Iranian connection that stalls a blocked TLS handshake instead of resetting it fast, opencode just sits there. If it hangs, set both before running:
OPENCODE_DISABLE_MODELS_FETCH=1 OPENCODE_DISABLE_AUTOUPDATE=1 opencode run -m uttapen/openai/gpt-5-nano "say hi"
This has nothing to do with the uttapen provider's model list in opencode.json — that comes from your own file, not from models.opencode.ai. The hang is intermittent, not constant; this same machine answered several times with no env vars at all, so try a plain run first.
When the balance can't cover the full max_tokens. uttapen doesn't reject the request. It lowers max_tokens to what your available balance covers (never below 1,024 output tokens) and returns the granted value in the X-Uttapen-Max-Tokens response header. You only get 402 when even 1,024 tokens are unaffordable. The side effect can be a reply cut short with finish_reason: "length". Top up, or lower limit.output so requests always fit your balance.
All error codes are in errors; other tools are in integrations.
Sign up with a phone number, top up your wallet from 100,000 toman and create a key. Free models work without a top-up — up to 50 requests a day. Create an API key →