OpenCode OpenAI Compatible Provider: Setting the Base URL in opencode.json

You ran /connect inside OpenCode, scrolled to Other, typed a provider ID, pasted your API key, and hit enter. OpenCode said it saved. You started a task. Nothing changed — the model list still shows the built-ins, and if you try to reference your model you get model not found. There was no base URL field anywhere in that flow, and that absence is the reason why.

That is the expected outcome, and it is documented. OpenCode's provider docs state the requirement as two numbered steps:

To add a provider you need to: 1. Add the API keys for the provider using the /connect command. 2. Configure the provider in your OpenCode config.

Step 1 is what you did. Step 2 is a file you write. /connect stores the key in ~/.local/share/opencode/auth.json and stops there — nothing in that flow writes a provider block for you.

So an opencode openai compatible provider is a block you write into opencode.json, not a form you fill in. Below: the shortest block that loads, the version difference that silently voids it, and the one command that makes your models appear.

The shortest opencode.json that works

OpenCode reads opencode.json from the project root. There are two config shapes in circulation, and they are not interchangeable.

v2 — note providers, plural:

{
  "$schema": "https://opencode.ai/config.json",
  "providers": {
    "aicomp": {
      "env": ["AICOMP_API_KEY"],
      "package": "@opencode/ai/providers/openai-compatible",
      "settings": {
        "baseURL": "https://aicomp.ai/v1"
      },
      "models": {
        "claude-sonnet-5": { "name": "Claude Sonnet 5" },
        "deepseek-v4-flash": { "name": "DeepSeek V4 Flash" }
      }
    }
  }
}

v1 — provider, singular, and the package is different:

{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "aicomp": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "aicomp",
      "options": {
        "baseURL": "https://aicomp.ai/v1",
        "apiKey": "{env:AICOMP_API_KEY}"
      },
      "models": {
        "claude-sonnet-5": { "name": "Claude Sonnet 5" }
      }
    }
  }
}

In OpenCode the provider block is keyed provider (singular) on v1 and providers (plural) on v2. Using the v1 key on a v2 build makes the entire block parse as an unknown key and get silently ignored — no error, no warning. The field names move too:

v1v2
Top-level keyprovider (singular)providers (plural)
Package fieldnpm: "@ai-sdk/openai-compatible"package: "@opencode/ai/providers/openai-compatible"
Base URLoptions.baseURLsettings.baseURL
Credentialoptions.apiKey: "{env:VAR}"env: ["VAR"] (ordered fallback)

Three things to get right before you save:

  1. The key never goes in the file. v2 lists the variable name in env; v1 interpolates it with {env:VAR}. Export it in the shell you launch OpenCode from — export AICOMP_API_KEY=sk-... — so it never lands in a committed file.
  2. The base URL stops at /v1. https://aicomp.ai/v1 and nothing after it. The provider package appends the resource path.
  3. The package string differs between versions. v1 is @ai-sdk/openai-compatible. v2 is @opencode/ai/providers/openai-compatible. A v1 package name in a v2 block, or the reverse, produces a provider that loads and then does nothing useful.

Verify the credential from outside OpenCode first. If this fails, nothing you do in the config matters:

export AICOMP_API_KEY=sk-your-gateway-key

curl -s https://aicomp.ai/v1/models \
  -H "Authorization: Bearer $AICOMP_API_KEY" \
  | jq -r '.data[].id' | head -40

Then start OpenCode in that directory and run the /models command. Your provider and its models appear in the picker only after that. Skipping /models is why a correct config looks broken: the values are loaded, the list just has not been refreshed.

You need a key before the code below runs. Create an account, generate a key, and copy the base URL (https://aicomp.ai/v1). Create one free →
Check current rates → Free to sign up · $1 minimum top-up · No prepayment

The OpenCode-specific parts of an OpenAI-compatible provider

/connect stores a credential, not a provider

The interactive flow is: /connect → scroll to the bottom → Other → enter a unique provider ID → enter the API key. Per the docs above, that is step 1 of 2. The key lands in ~/.local/share/opencode/auth.json.

So the sequence that works is both steps, in order:

# 1. register the credential interactively
opencode            # then /connect -> Other -> "aicomp" -> paste key

# 2. write the provider block into ./opencode.json (the two blocks above)

# 3. refresh the picker inside the TUI
#    /models

If you did step 1 and stopped, you have a key in a store and no provider that uses it.

Model references are providerID/modelID

The providers object is indexed by provider ID, and that same ID is the prefix in every model reference. With the config above, the provider ID is aicomp, so the models are aicomp/claude-sonnet-5 and aicomp/deepseek-v4-flash. Anywhere OpenCode asks for a model — the picker, a --model flag, a per-agent override — the string carries the prefix.

This is where it gets sharp: a small number of catalogue IDs already contain a slash (

12 of 839 catalogue model IDs (1.4%) already contain a slash. Every one of those makes a providerID/modelID reference ambiguous on sight, because the model half carries a slash of its own. The first slash is always the separator; everything after it is the model ID verbatim.

). They are rare but they are real — OpenCode's own docs use "google/gemma-3n-e4b" as a model key in their LM Studio example, so a slash inside the model half is expected rather than an error. A gateway model named vendor/model produces a reference like aicomp/vendor/model, and it is genuinely ambiguous on sight which slash separates provider from model and which is part of the model name. The rule is that the first slash is always the separator — the prefix is the provider ID you wrote, and everything after it is the model ID verbatim, including any slashes it carries.

Per-model limits belong in the same block. OpenCode accepts a limit object with context and output, which is where you stop a long response being cut short:

"models": {
  "claude-sonnet-5": {
    "name": "Claude Sonnet 5",
    "limit": { "context": 200000, "output": 16384 }
  }
}

settings.baseURL only exists if the package supports it

The base URL is not a top-level OpenCode field in v2 — it lives under settings as settings.baseURL, and it is package-specific. It applies only when the package you named actually reads it. Set @opencode/ai/providers/openai-compatible and the setting is honoured. Name a package that does not implement it and the field is inert: no error, no warning, requests just go to the package's default endpoint.

That is why a config can look perfect and still bill the wrong place. Confirm the request in your gateway usage log rather than trusting a successful task.

headers and body work at two levels

Both v1 (options.headers) and v2 accept custom headers, and v2 adds body. The useful part is scope: you can set them at the provider level, where they apply to every model, or at the model or variant level, where they override for one entry. A provider that needs a specific header on one model only does not force you to duplicate the provider block.

{
  "providers": {
    "aicomp": {
      "env": ["AICOMP_API_KEY"],
      "package": "@opencode/ai/providers/openai-compatible",
      "settings": { "baseURL": "https://aicomp.ai/v1" },
      "headers": { "HTTP-Referer": "https://your-app.example" },
      "models": {
        "claude-sonnet-5": {
          "name": "Claude Sonnet 5",
          "headers": { "X-One-Model-Only": "true" }
        }
      }
    }
  }
}

env is an ordered list, not a single name

v2's env takes an array of environment variable names and checks them in order. That is a fallback chain: if the first is unset, OpenCode tries the next. It is also a quiet failure source — if you renamed your variable and left the old name first, OpenCode finds nothing and you get an auth error with a key that is definitely exported under some name.

Failure modes

SymptomWhat is actually wrongFix
/connect succeeded but nothing changed/connect only stores a credential; the provider block was never writtenAdd the providers (or provider) block to opencode.json by hand
Provider missing from /models after editing the filePicker not refreshed, or the file is not in the project rootRun /models; confirm opencode.json sits at the project root, not in a subdirectory
401 with the key exportedv2 env lists a variable name that is not the one you exported, or the shell OpenCode launched from never had itMatch the name in env exactly; export in the same shell
404 on every callBase URL carries a resource path, or a settings.baseURL the named package ignoresEnd the URL at /v1; confirm the package is @opencode/ai/providers/openai-compatible (v2)
model not foundReference missing the prefix, or a catalogue ID with its own slash mis-splitUse providerID/modelID; the first slash separates, the rest belongs to the model
Model list emptyModels not declared under models, or /models never runDeclare each model ID, then run /models
Requests succeed, gateway log emptysettings.baseURL inert because the package does not support itUse the openai-compatible package; verify in the usage log
Long responses cut offModel-level output cap, not a config errorCheck the model's published output limit on /v1/models

What a coding session costs here

OpenCode re-reads whatever is in scope on every turn, so a twenty-turn session pays for the same files twenty times on the input side. The output side is smaller per turn but repeats: every proposed edit is a full response, and every failed command buys another one. A realistic day lands near 180k input tokens and 55k output tokens. Over 20 working days, 3.6M input and 1.1M output.

Per 1M input / output tokens,
ModelGateway rate
in / out per 1M tokens
One day20 days
claude-sonnet-5$1.00 / $5.00$0.46$9.10
gpt-5.6-sol$2.5 / $15.00$1.27$25.50
deepseek-v4-flash$0.22 / $0.66$0.08$1.52
glm-5.3$0.70 / $2.2$0.25$4.94
qwen3.8-max$1.00 / $3.00$0.34$6.90

One day = 180k input + 55k output on this page's workload. 20 days = 3.6M input and 1.1M output. Rates checked 2026-09-26 — gateway rates move with upstream promotions, so verify the current number in your dashboard before committing to a budget.

Because the reference format is providerID/modelID, switching models is a one-token edit — same endpoint, same key, same config block. That makes the most effective lever here a per-phase split rather than a per-model one: declare a strong model and an inexpensive one under the same provider, then pick per task instead of paying the strong rate for mechanical edits.

In 93% of the 216 models publishing both rates (200 of them), output costs more per token than input. The other 623 raw catalogue entries are dropped by cleaning — test entries, dated snapshots and non-chat variants. A session that re-reads the same files every turn lands on the expensive side of that split.

FAQ

FAQ

Does /connect configure the provider for me?

No. The interactive flow registers a credential and a provider ID, and OpenCode's own prompt says so — it stores the key and tells you to add the configuration to opencode.json yourself. Both steps are required. /connect alone leaves you with a key in a store and no provider referencing it.

Is it provider or providers in opencode.json?

Depends on your version. v1 uses provider, singular, with npm: "@ai-sdk/openai-compatible" and an options object holding baseURL and apiKey. v2 uses providers, plural, with package, an env array, and a settings.baseURL. Using the v1 shape on a v2 build means your block is parsed as an unknown key and silently ignored.

Why does my model show as not found when it is in the config?

Because the reference needs the provider prefix. Models are addressed as providerID/modelID, where the prefix is the key you used in the providers object. claude-sonnet-5 is not a valid reference; aicomp/claude-sonnet-5 is. If the catalogue ID itself contains a slash, the first slash is still the separator — everything after it is the model ID as published.

Where does baseURL go in v2?

Under settings, as settings.baseURL, and it is package-specific: the setting applies only if the package you named implements it. With @opencode/ai/providers/openai-compatible it is honoured. With a package that does not read it, the field is inert and traffic goes to that package's default endpoint — which is why a successful task is not proof of correct routing.

How do I make my custom models show up?

Declare them under models in the provider block, then run the /models command inside the TUI. The picker is populated from the declared models on refresh; it is not a live call to your endpoint. If you add a model to the file without running /models, it exists and is usable by reference but will not appear in the list.

Can I set headers for one model only?

Yes. headers (and body, in v2) can sit at the provider level, where they apply to every model, or on an individual model or variant, where they override for that entry. That makes provider-specific requirements a one-line addition rather than a duplicated provider block.

Related

Get API access

Get API access