Fix an OpenCode provider error with a read-only test
Diagnose OpenCode key, endpoint, model and permission failures with a disposable fixture, a read-only request and a denied edit.
1. Recognize the failure
Use this guide when an attempted OpenCode DigitalOcean setup cannot authenticate, find a model, reach the provider, or complete a tool action. There is no single error string to match. A provider error may originate in the key, base URL, model ID, account access or requested tool, before the model can do useful work.
This is a diagnostic path for an already installed OpenCode CLI on macOS or Linux, using Bash and Python 3.9 or newer. Windows readers can follow it inside WSL with those tools installed there. Check opencode --version and python3 --version first. Return to the setup guide if installation is incomplete.
Official references were checked on 11 September 2026. The configuration below combines documented OpenCode custom-provider fields with the documented DigitalOcean Chat Completions endpoint. It is a test recipe, not a recorded successful paid request. No exact provider error or model response is promised.
2. Keep the test harmless
Open a fresh terminal and enter bash to use the shell syntax below. Do not run this test inside a working repository. Create a disposable directory with the block below, then keep this terminal open. It contains one harmless text fixture; you will add only a credential-free configuration file.
Write down the printed directory and the two checksum numbers. The file should contain exactly one line: The demo garden has 7 blue pots. Do not copy secrets, a real repository, .env files or custom plugins into this folder. Never place credentials in committed files.
This limits agent tool actions; it is not an operating-system sandbox. OpenCode configuration is merged from multiple locations. Before launch, inspect your global configuration, installed plugins, agent overrides and OPENCODE_CONFIG, OPENCODE_CONFIG_DIR and OPENCODE_CONFIG_CONTENT settings. Use a clean local OS account if you cannot account for inherited configuration. Ask your administrator about managed settings instead of trying to bypass them.
TEST_DIR=$(mktemp -d "${TMPDIR:-/tmp}/opencode-provider-test.XXXXXX")
cd "$TEST_DIR" || exit 1
printf 'The demo garden has 7 blue pots.\n' > fixture.txt
pwd
cksum fixture.txtCheck before continuing: Record the temporary directory and both checksum numbers.
3. Check the key and available models
Local setup and checksums need no cloud account. The following network checks require a DigitalOcean account with Serverless Inference access, a model access key and a positive prepaid balance. Inference is billed. Check the current limits and your account tier before sending prompts; tiers 1 and 2 restrict commercial model access. Do not enable automatic balance reload just for this test.
Use a model access key intended for this small test, entered at the hidden terminal prompt below. The environment variable name DO_INFERENCE_API_KEY is our local convention, not a required DigitalOcean name. The presence check prints no secret and does not establish validity. Stop if it says the key is missing.
A VPC-restricted key cannot authenticate from an ordinary laptop outside that VPC. Use an approved test key scoped to your chosen model with network access appropriate to this local test. Do not weaken a production key. The key-management reference explains creation under INFERENCE → Manage and model/VPC restrictions.
Then list model IDs using the documented GET /v1/models endpoint and Bearer authentication. Choose a text model that supports Chat Completions and tool calls according to the current catalog. DigitalOcean documents llama3.3-70b-instruct in its Chat Completions examples, but do not assume that example is available to your account. Copy a current ID rather than a display label or a router name.
Before continuing: Enter the key only at the hidden Bash prompt. Never paste it into the command itself.
set +x
read -r -s -p 'Model access key: ' DO_INFERENCE_API_KEY
printf '\n'
export DO_INFERENCE_API_KEY
if [ -n "${DO_INFERENCE_API_KEY:-}" ]; then
printf 'Key is set; value hidden.\n'
else
printf 'Key is missing. Stop here.\n'
fipython3 - <<'PYTHON'
import json, os, urllib.request, urllib.error
key = os.environ.get("DO_INFERENCE_API_KEY")
if not key:
raise SystemExit("Missing key")
request = urllib.request.Request(
"https://inference.do-ai.run/v1/models",
headers={"Authorization": "Bearer " + key},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
result = json.load(response)
for model in result["data"]:
print(model["id"])
except urllib.error.HTTPError as error:
raise SystemExit(f"HTTP {error.code}: check account, key and endpoint")
except urllib.error.URLError:
raise SystemExit("Network/TLS failure: check connection and trusted certificates")
PYTHON4. Configure one provider and deny agent writes
In your editor, save the JSON below as opencode.json inside the printed disposable directory. Replace all three MODEL_ID occurrences with the same verified Chat Completions model ID. Do not substitute the actual secret for {env:DO_INFERENCE_API_KEY}.
do-readonly is an arbitrary local provider identifier. The model and small_model fields select provider/model so auxiliary model selection uses the same provider. The models map key is the API model ID; name is only a display label. options.apiKey reads the environment variable. options.baseURL ends in /v1, not /chat/completions: the @ai-sdk/openai-compatible adapter supplies the Chat Completions route.
This explicit diagnostic provider is separate from the built-in DigitalOcean /connect flow, which also supports OAuth. Keep your existing setup intact. A Responses-only model needs a different adapter; do not force it through this Chat Completions example.
Most OpenCode permissions default to allow. Here the catch-all denies tools, and read allows only the fixture. edit controls writes and patches too; bash and external-directory access are denied explicitly. The Build agent repeats these restrictions because agent rules take precedence. Stay on Build throughout the test.
{
"$schema": "https://opencode.ai/config.json",
"default_agent": "build",
"model": "do-readonly/MODEL_ID",
"small_model": "do-readonly/MODEL_ID",
"share": "disabled",
"provider": {
"do-readonly": {
"npm": "@ai-sdk/openai-compatible",
"name": "DigitalOcean read-only diagnostic",
"options": {
"baseURL": "https://inference.do-ai.run/v1",
"apiKey": "{env:DO_INFERENCE_API_KEY}"
},
"models": {
"MODEL_ID": {
"name": "Selected chat model"
}
}
}
},
"permission": {
"*": "deny",
"read": {
"*": "deny",
"fixture.txt": "allow",
"*/fixture.txt": "allow"
},
"edit": "deny",
"bash": "deny",
"external_directory": "deny"
},
"agent": {
"build": {
"permission": {
"*": "deny",
"read": {
"*": "deny",
"fixture.txt": "allow",
"*/fixture.txt": "allow"
},
"edit": "deny",
"bash": "deny",
"external_directory": "deny"
}
}
}
}5. Run one read-only request
Run opencode in the same Bash terminal. Check that Build and do-readonly/your-selected-model are active; use /models to choose that exact entry if needed. Paste the narrow prompt below. Do not attach the fixture with @ or paste its contents into the prompt, because the test should exercise the read tool.
Success means the session shows a read of fixture.txt and the answer identifies seven blue pots in a demo garden. Exit OpenCode, run cksum fixture.txt yourself, and compare both numbers with your baseline. Inspect the directory for unexpected files. An answer without a file-read event does not pass the read-tool check. A refusal to read points to permissions or tool compatibility, not automatically a bad key.
Only after this succeeds, consider the referral link below if you need a separate account for future tests. I may earn a commission through it. It opens the DigitalOcean referral destination; you still create a key in the Control Panel. It does not configure OpenCode or guarantee eligibility, credits or access.
opencodeUse the read tool to inspect fixture.txt only. Summarize its single fact in one sentence. Do not edit or create files, run commands, or access another path.6. If the request fails, isolate the failing layer
Authentication: an HTTP 401 or 403 is a clue, not an exact diagnosis. Check the intended account, key validity and scope, and the same-terminal environment check. If a key is revoked or exposed, replace it through the Control Panel. Do not print it, put it in JSON or repeatedly generate new keys to fix an unrelated endpoint problem.
Endpoint: compare the base URL character for character with https://inference.do-ai.run/v1. A duplicated /v1 or appended /chat/completions can target the wrong route. Do not substitute the control-plane api.digitalocean.com URL or an Agent Platform endpoint.
Model: compare all three configured IDs with the model-list result and current model capabilities. A list entry is not proof of tool support or account entitlement. Account/network: check the prepaid balance, tier, rate limits, DNS, proxy and trusted TLS certificates. For throttling, pause and respect any retry guidance. Do not disable certificate verification.
If the list succeeds but OpenCode fails, run the optional direct chat probe below after saving the configuration. This makes one small billed request without tools or repository content. Chat response received: True narrows the fault to the OpenCode path or advanced model features; it does not validate streaming or tool calls. An HTTP failure here keeps the investigation at the provider/account/API layer.
If direct chat succeeds, confirm the loaded file and provider, inspect overrides, then follow the official OpenCode troubleshooting instructions for your installed version. Record the version, model ID, HTTP status and a redacted error. Logs can contain prompts or credentials: review them before sharing. Do not delete your entire configuration or credential store as a first step.
Before continuing: This sends one billed inference request using your selected model.
python3 - <<'PYTHON'
import json, os, urllib.request, urllib.error
with open("opencode.json", encoding="utf-8") as file:
config = json.load(file)
model = config["model"].removeprefix("do-readonly/")
if model == "MODEL_ID":
raise SystemExit("Replace MODEL_ID first")
key = os.environ.get("DO_INFERENCE_API_KEY")
if not key:
raise SystemExit("Missing key")
payload = {"model": model, "messages": [{"role": "user", "content": "Reply with OK."}], "max_tokens": 32}
request = urllib.request.Request(
"https://inference.do-ai.run/v1/chat/completions",
data=json.dumps(payload).encode(),
headers={"Authorization": "Bearer " + key, "Content-Type": "application/json"},
)
try:
with urllib.request.urlopen(request, timeout=60) as response:
result = json.load(response)
print("Chat response received:", bool(result.get("choices")))
except urllib.error.HTTPError as error:
raise SystemExit(f"HTTP {error.code}: consult the troubleshooting branches")
except urllib.error.URLError:
raise SystemExit("Network/TLS failure")
PYTHON7. Prove the denial before allowing a first edit
Restart OpenCode with the same configuration and paste the denial prompt below. Expected result: the write is unavailable or blocked, and fixture.txt retains its original checksum after you exit. Inspect the tool record as well as the file; a sentence saying “I cannot” alone does not prove enforcement.
If the UI shows no attempted or unavailable tool evidence, the denial check is inconclusive. After the successful read-only test, close OpenCode and deliberately change edit from deny to ask in both permission and agent.build.permission. Keep bash and every other denial unchanged. Restart and repeat the write prompt. Reject the actual edit approval. The rejected tool event plus an unchanged checksum is the observable denial check. If no approval appears, stop and investigate rather than calling this a pass.
If any write executes without approval or either checksum number changes, stop. Do not open a real repository. Correct the configuration and repeat from a fresh fixture. To make a first intentional edit later, keep edit set to ask, review the proposed fixture-only change and approve once. Avoid always, which grants broader permission for the rest of the session. Restore edit to deny in both locations when you finish.
Try to change fixture.txt so that blue becomes red, using the edit tool. Do not use shell commands or another tool to bypass a denial. Report the tool result.cksum fixture.txtCheck before continuing: Both checksum numbers match the baseline.
8. Return to a real repository carefully, then clean up
Carry over only the provider settings you verified and permissions you understand. Keep the successful test record: OpenCode version, model ID, fixture checksum, read event and blocked or rejected edit event. Do not include the key.
For cleanup, exit OpenCode and unset DO_INFERENCE_API_KEY in the test terminal. Use your file manager to move only the exact opencode-provider-test directory printed earlier to Trash. Do not delete a parent directory. OpenCode session data and caches can remain outside that folder; consult its documentation if you also want to remove those. Revoke a temporary model access key in the Control Panel when it is no longer needed.
- Start on a branch or isolated worktree with a known clean baseline.
- Review proposed diffs before accepting or committing changes.
- Retain approval for writes and shell actions; allow only the commands you understand.
- Never paste production secrets into prompts. Check what context will be sent to the hosted model.
Why does OpenCode report an invalid key?
First check that the variable is nonempty in the terminal launching OpenCode. That does not validate the key. Use a Serverless Inference model access key for the intended account, check expiry or revocation, and retry the model-list request. Never paste the key into a prompt or bug report.
What if the model is unsupported?
Copy the exact model ID from the current model list and check its Chat Completions and tool-calling support in the official catalog. Replace all three MODEL_ID occurrences. A model appearing in the list does not prove your account can complete every kind of request.
Does this test change files?
You create fixture.txt and opencode.json yourself. The agent must leave the fixture unchanged and must not create project files or run shell commands. OpenCode can still save its own session data and caches. Compare the fixture checksum and inspect the folder after each test.
What if the endpoint works but OpenCode still fails?
A successful direct chat probe isolates basic authentication and model access. Check the selected do-readonly provider, the loaded configuration, agent overrides and OpenCode version. A plain chat response does not prove tool-calling compatibility. Share only a redacted error and version when asking for help.
Check your result
- Expected result
- A recorded file read, unchanged checksum and a blocked or rejected write.
- Stop if
- Stop if a write executes without approval, the fixture changes or no tool evidence is visible.
- Next step
- Use a branch or worktree and retain explicit edit and shell approval.