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
/connectcommand. 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:
| v1 | v2 | |
|---|---|---|
| Top-level key | provider (singular) | providers (plural) |
| Package field | npm: "@ai-sdk/openai-compatible" | package: "@opencode/ai/providers/openai-compatible" |
| Base URL | options.baseURL | settings.baseURL |
| Credential | options.apiKey: "{env:VAR}" | env: ["VAR"] (ordered fallback) |
Three things to get right before you save:
- 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. - The base URL stops at
/v1.https://aicomp.ai/v1and nothing after it. The provider package appends the resource path. - 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.
https://aicomp.ai/v1).
Create one free →
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 (
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
| Symptom | What is actually wrong | Fix |
|---|---|---|
/connect succeeded but nothing changed | /connect only stores a credential; the provider block was never written | Add the providers (or provider) block to opencode.json by hand |
Provider missing from /models after editing the file | Picker not refreshed, or the file is not in the project root | Run /models; confirm opencode.json sits at the project root, not in a subdirectory |
401 with the key exported | v2 env lists a variable name that is not the one you exported, or the shell OpenCode launched from never had it | Match the name in env exactly; export in the same shell |
404 on every call | Base URL carries a resource path, or a settings.baseURL the named package ignores | End the URL at /v1; confirm the package is @opencode/ai/providers/openai-compatible (v2) |
model not found | Reference missing the prefix, or a catalogue ID with its own slash mis-split | Use providerID/modelID; the first slash separates, the rest belongs to the model |
| Model list empty | Models not declared under models, or /models never run | Declare each model ID, then run /models |
| Requests succeed, gateway log empty | settings.baseURL inert because the package does not support it | Use the openai-compatible package; verify in the usage log |
| Long responses cut off | Model-level output cap, not a config error | Check 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.
| Model | Gateway rate in / out per 1M tokens | One day | 20 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.
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
- Base URL not working — the doubled-path, wrong-vendor and inert-setting failures
- OpenAI-compatible error codes — what each status tells you, in the order they fire
- LiteLLM model name mapping — the same prefix problem in a different tool
- OpenAI SDK with a custom endpoint — the raw-client version of this setup
- Context length exceeded — when the real error is context, not config
- All tools with one endpoint — the same override across editors and CLIs
- Cost calculator — put your own token volumes against the rates above