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:

  • npm must 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 get 404.
  • uttapen is the provider id inside opencode. Any name works, as long as model and small_model use the same one.
  • options.apiKey with {env:UTTAPEN_API_KEY} reads the key from the environment. To let opencode store the key instead, drop that line, run /connect in opencode, choose Other, enter the provider id uttapen and paste the key.
  • The keys under models must be exact uttapen model ids; opencode sends that string as the request's model.
  • limit.output becomes the max_tokens opencode 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_model is 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:

RoleModel id
Strong main modelanthropic/claude-sonnet-5, openai/gpt-5
Budget main modelqwen/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.output to about 8192 for 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 at 16000.
  • Point small_model at a cheap model with limit.output around 1024.
  • If you don't want to set a number per model, OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX is a global ceiling: opencode sends the smaller of it and limit.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

ErrorCause in opencodeFix
401 invalid_api_keyUTTAPEN_API_KEY isn't set in the shell that launched opencode, or the key was revokedRun echo $UTTAPEN_API_KEY in that terminal, or store the key again with /connect
404 model_not_foundA 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 dotCopy the id from /en/models (the 404 also lists the closest ids it guessed, in uttapen.suggestions)
404 not_foundnpm is @ai-sdk/openai, so the request went to /v1/responsesUse @ai-sdk/openai-compatible
402 insufficient_balanceThe balance can't cover even 1,024 output tokens for this request; concurrent holds add upTop up; set limit.output and small_model as above
429The key's per-minute or monthly cap is reached, or the model is briefly rate-limitedRead the error code (rate limits); raise the key's rate_limit_rpm
Reply stops mid-sentencefinish_reason: "length": limit.output is low, or uttapen lowered the ceilingSee below
opencode just sits there — no error, no replyStuck in init, before it ever reaches uttapenSee 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.

No API key yet?

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 →