Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AiOverviewControlSettings.qml
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ PluginSettings {
{ id:"perplexity", name:"Perplexity", icon:"auto_awesome", mode:"informational", requirement:"none", envVar:"", note:"No public read-only quota API" },
{ id:"cursor", name:"Cursor", icon:"mouse", mode:"informational", requirement:"none", envVar:"", note:"No public read-only quota API" },
{ id:"cline", name:"Cline", icon:"code_blocks", mode:"informational", requirement:"none", envVar:"", note:"No public read-only quota API" },
{ id:"opencode", name:"OpenCode", icon:"open_in_new", mode:"informational", requirement:"none", envVar:"", note:"Usage belongs to configured upstream providers" },
{ id:"opencode", name:"OpenCode Go", icon:"open_in_new", mode:"telemetry", requirement:"API key or CLI login", envVar:"OPENCODE_API_KEY", note:"Uses OPENCODE_API_KEY or the credential saved by `opencode auth login` to ~/.local/share/opencode/auth.json. Reads OpenCode Zen's /zen/go/v1/usage for live 5h/weekly/monthly quota; shows when balance fallback is enabled; falls back to /zen/go/v1/models on failure." },
{ id:"kiro", name:"Kiro", icon:"tune", mode:"informational", requirement:"none", envVar:"", note:"Subscription-only IDE; no public API" },
{ id:"warp", name:"Warp", icon:"speed", mode:"informational", requirement:"none", envVar:"", note:"No public read-only quota API" },
{ id:"amp", name:"Amp", icon:"bolt", mode:"informational", requirement:"none", envVar:"", note:"No public read-only quota API" }
Expand Down
3 changes: 2 additions & 1 deletion AiOverviewControlWidget.qml
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ PluginComponent {
grok: "xAI",
perplexity: "Perplexity",
cline: "Cline",
opencode: "OpenCode",
opencode: "OpenCode Go",
kilo: "Kilo",
kiro: "Kiro",
amp: "Amp",
Expand Down Expand Up @@ -850,6 +850,7 @@ PluginComponent {
if (providerId === "glm" || providerId === "zhipu" || providerId === "zai") return Theme.primary;
if (providerId === "minimax") return Theme.success;
if (providerId === "commandcode" || providerId === "cmd" || providerId === "cmdcode") return Theme.primary;
if (providerId === "opencode") return Theme.secondary;
if (providerId === "qwen" || providerId === "dashscope" || providerId === "alibaba") return Theme.warning;
if (providerId === "nvidia" || providerId === "nim") return Theme.success;
if (providerId === "cloudflare") return Theme.warning;
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### OpenCode Go provider

- New **OpenCode Go** (opencode.ai) provider surfaces the live 5-hour, weekly, and monthly usage windows via OpenCode Zen's `/zen/go/v1/usage`, authenticated with `OPENCODE_API_KEY` or the CLI credential in `${XDG_DATA_HOME:-$HOME/.local/share}/opencode/auth.json`. The endpoint is young, so malformed or unavailable quota data degrades to the documented `/zen/go/v1/models` auth-only check — never a fabricated percentage. `useBalance: true` is shown as **balance fallback enabled**; the endpoint does not expose an amount, so none is claimed. `opencode` moves from the informational tier (no quota API) to a full telemetry provider, and `get-provider-health` gained a matching `opencode` case.

## 1.13.3 - 2026-08-26

### Hermetic test suites
Expand Down
42 changes: 28 additions & 14 deletions docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ This document is the authoritative reference for adapter authors. It records, fo
- the **current flagship model(s)** and recent **changelog** highlights,
- and the **official documentation source** used.

Reviewed 2026-08-25. Provider APIs change; re-check the linked sources before changing an adapter.
Reviewed 2026-08-26. Provider APIs change; re-check the linked sources before changing an adapter.

## Coverage levels

Every provider maps to exactly one coverage level. The level dictates what the widget can truthfully render.

| Level | Meaning | Example providers |
| --- | --- | --- |
| **Quota** | Real `usedPercent` + reset window from a protocol/API. | `codex`, `copilot`, `antigravity`, `openrouter`, `zai`, `glm`, `fireworks` (with account ID) |
| **Quota** | Real `usedPercent` + reset window from a protocol/API. | `codex`, `copilot`, `antigravity`, `openrouter`, `zai`, `glm`, `fireworks` (with account ID), `commandcode`, `opencode` |
| **Balance** | Remaining prepaid balance / credits in real currency. | `kimi`, `deepseek` |
| **Analytics** | Consumption counters (requests/tokens/neurons/cost) with no remaining-quota value. | `cloudflare` (GraphQL), `9router`, `claude` (local), `pi` (local), `hermes` (local) |
| **Auth / configured** | Validates credentials with a read-only endpoint when possible; otherwise reports only that a credential is configured and states the limitation. No usage numbers. | `gemini`, `mistral`, `nvidia`, `qwen`, `byteplus`, `groq`, `cohere`, `replicate`, `together`, `minimax`, `xai`, `kilo`, `ai21` |
| **Local runtime** | Local process / installed models. | `ollama`, `vertexai` (gcloud) |
| **Informational** | No public read-only API at all; the card just links to the dashboard. | `perplexity`, `cursor`, `cline`, `opencode`, `kiro`, `warp`, `amp` |
| **Informational** | No public read-only API at all; the card just links to the dashboard. | `perplexity`, `cursor`, `cline`, `kiro`, `warp`, `amp` |

## Provider kinds

Expand Down Expand Up @@ -189,6 +189,17 @@ The matrix below summarises the **authentication/billing surface** for every sup
<td><a href="https://commandcode.ai/docs/provider">Provider API docs</a></td>
</tr>
<tr>
<td><code>opencode</code></td>
<td>Quota</td>
<td>✅ <code>GET /zen/go/v1/models</code></td>
<td>✅ <code>/zen/go/v1/usage</code> (5h + weekly + monthly)</td>
<td>✅ OpenCode Go $10/mo</td>
<td>—</td>
<td><code>OPENCODE_API_KEY</code> or CLI <code>~/.local/share/opencode/auth.json</code></td>
<td><a href="https://opencode.ai/zen">opencode.ai/zen</a></td>
<td><a href="https://opencode.ai/docs/go">opencode.ai/docs/go</a></td>
</tr>
<tr>
<td><code>kimi</code></td>
<td>Balance / Quota</td>
<td>✅ <code>GET /v1/models</code></td>
Expand Down Expand Up @@ -432,17 +443,6 @@ The matrix below summarises the **authentication/billing surface** for every sup
<td><a href="https://cline.bot">cline.bot</a></td>
</tr>
<tr>
<td><code>opencode</code></td>
<td>Informational</td>
<td>❌</td>
<td>❌</td>
<td>—</td>
<td>—</td>
<td>—</td>
<td>upstream providers</td>
<td><a href="https://opencode.ai">opencode.ai</a></td>
</tr>
<tr>
<td><code>warp</code></td>
<td>Informational</td>
<td>❌</td>
Expand Down Expand Up @@ -601,6 +601,20 @@ Detailed adapter notes for the focus providers (Gemini, Cloudflare, Mistral, GLM
| **Dashboard** | [commandcode.ai/billing](https://commandcode.ai/billing) — billing, plan, and credit top-ups. |
| **Adapter** | `fetch_commandcode_native` — `/alpha/billing/credits` + `/alpha/whoami` + `/alpha/billing/subscriptions`, with documented-endpoint fallback. |

### OpenCode Go

| | |
| --- | --- |
| **API base** | `https://opencode.ai/zen/go/v1` — OpenCode Zen's Go-plan quota surface, alongside the OpenAI-compatible `/zen/v1` inference API. |
| **Credentials** | `OPENCODE_API_KEY` (preferred when available to DMS), or the `key` in CLI-owned `${XDG_DATA_HOME:-$HOME/.local/share}/opencode/auth.json` (saved by `opencode auth login`). The adapter follows this XDG path on Linux, the plugin's supported platform. The same key created in OpenCode Studio/Zen drives both inference and quota introspection. |
| **Auth** | `Authorization: Bearer ***` |
| **Quota / balance** | `/zen/go/v1/usage` returns `rollingUsage` (5h window), `weeklyUsage`, and `monthlyUsage`, each `{status, resetInSec, usagePercent}` — percentages are computed server-side, so the adapter never derives them from raw byte/token counts. The adapter accepts a quota response only when all three windows have valid percentage/reset values; otherwise it falls back to auth-only status. `resetInSec` is seconds-from-now, converted to an absolute ISO 8601 timestamp. `useBalance: true` is rendered as **balance fallback enabled**; the endpoint does not return the balance amount, so none is invented. |
| **Plans** | **OpenCode Go** $10/mo — $12 of usage per 5-hour window, $30 weekly, $60 monthly. |
| **Billing** | Flat monthly subscription; no PAYG surfaced through this endpoint. |
| **Stability** | The `/zen/go/` namespace shipped alongside the Go plan (opencode PR #16513) and has no documented versioned contract. Adapter degrades to `/zen/go/v1/models` (auth-only) on failure and emits a clearly-labeled "quota endpoint unavailable" note; it never fabricates a percentage. A missing/invalid key returns `401 AuthError`; a valid key without a Go subscription returns `403 EntitlementError` — both surface as hard errors, not soft notes. |
| **Dashboard** | [opencode.ai/zen](https://opencode.ai/zen) — Zen usage and Go plan management. |
| **Adapter** | `fetch_opencode_native` — `/zen/go/v1/usage`, with `/zen/go/v1/models` fallback. |

### Kimi (Moonshot AI)

| | |
Expand Down
11 changes: 10 additions & 1 deletion providers/get-provider-health
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ has_commandcode_credentials() {
&& jq -e '(.apiKey | type) == "string" and (.apiKey | length) > 0' "$auth_file" >/dev/null 2>&1
}

has_opencode_credentials() {
# Match the CLI's XDG data directory on Linux DMS sessions.
local auth_file="${XDG_DATA_HOME:-$HOME/.local/share}/opencode/auth.json"
has_any_env OPENCODE_API_KEY && return 0
[ -r "$auth_file" ] \
&& jq -e '((."opencode".key // ."opencode-go".key // ."opencode_go".key // .apiKey) | type) == "string" and (length > 0)' "$auth_file" >/dev/null 2>&1
}

status_for() {
local provider="$1"
local status="ready"
Expand All @@ -41,6 +49,7 @@ status_for() {
kimi|moonshot) has_any_env MOONSHOT_API_KEY KIMI_API_KEY KIMI_CODING_API_KEY || { status="missing"; detail="MOONSHOT_API_KEY, KIMI_API_KEY, or KIMI_CODING_API_KEY"; } ;;
minimax) has_any_env MINIMAX_API_KEY || { status="missing"; detail="MINIMAX_API_KEY"; } ;;
commandcode|cmd|cmdcode) has_commandcode_credentials || { status="missing"; detail="COMMAND_CODE_API_KEY or ~/.commandcode/auth.json"; } ;;
opencode) has_opencode_credentials || { status="missing"; detail="OPENCODE_API_KEY or ~/.local/share/opencode/auth.json"; } ;;
glm|zhipu) has_any_env GLM_API_KEY ZHIPU_API_KEY || { status="missing"; detail="GLM_API_KEY or ZHIPU_API_KEY"; } ;;
zai|z.ai) has_any_env ZAI_API_KEY GLM_API_KEY ZHIPU_API_KEY || { status="missing"; detail="ZAI_API_KEY"; } ;;
mistral) has_any_env MISTRAL_API_KEY || { status="missing"; detail="MISTRAL_API_KEY"; } ;;
Expand All @@ -60,7 +69,7 @@ status_for() {
ai21) has_any_env AI21_API_KEY || { status="missing"; detail="AI21_API_KEY"; } ;;
pi) command -v pi >/dev/null 2>&1 || [ -d "$HOME/.pi/agent/sessions" ] || { status="missing"; detail="pi CLI or ~/.pi/agent/sessions"; } ;;
hermes) [ -f "${HERMES_HOME:-$HOME/.hermes}/state.db" ] || command -v hermes >/dev/null 2>&1 || { status="missing"; detail="hermes CLI or ~/.hermes/state.db"; } ;;
perplexity|cursor|cline|opencode|kiro|warp|amp)
perplexity|cursor|cline|kiro|warp|amp)
status="info"
detail="No public read-only quota API"
;;
Expand Down
109 changes: 108 additions & 1 deletion providers/get-provider-usage
Original file line number Diff line number Diff line change
Expand Up @@ -1606,6 +1606,113 @@ fetch_commandcode_native() {
"commandcode.ai/billing"
}

opencode_api_key() {
# OpenCode Zen issues one API key from OpenCode Studio; a local CLI login
# (opencode auth login) persists the same key to this CLI-owned file. Path
# and key shape verified against opencode's docs and the opencode CLI
# behavior on Linux. Honor XDG_DATA_HOME for non-default DMS sessions.
local auth_file="${XDG_DATA_HOME:-$HOME/.local/share}/opencode/auth.json"
if [ -n "${OPENCODE_API_KEY:-}" ]; then
printf '%s' "$OPENCODE_API_KEY"
elif [ -r "$auth_file" ]; then
# Accept either the {"opencode":{"key":...}} shape (current CLI) or a
# legacy flat {"apiKey":...} shape so the bridge still works for users
# who hand-edited the file before the rename.
jq -r '."opencode".key // ."opencode-go".key // ."opencode_go".key // .apiKey // empty' "$auth_file" 2>/dev/null || true
fi
}

fetch_opencode_native() {
# OpenCode Go (opencode.ai) — coding-agent subscription quota via OpenCode
# Zen. /zen/go/v1/usage returns percent-already-computed 5h/weekly/monthly
# windows. The endpoint is young (opencode PR #16513), so on failure we
# degrade to /zen/go/v1/models (auth-only check), matching the Command Code
# adapter's fallback shape.
local key
key="$(opencode_api_key)"
if [ -z "$key" ]; then
json_error opencode opencode-api 2 provider "OpenCode credentials are unavailable. Set OPENCODE_API_KEY or run 'opencode auth login' so ~/.local/share/opencode/auth.json has a key."
return 0
fi

local tmp_body http_status
tmp_body="$(mktemp)"
http_status="$(curl -sS --max-time 8 -o "$tmp_body" -w '%{http_code}' \
-H "Authorization: Bearer ${key}" \
-H "Accept: application/json" \
"https://opencode.ai/zen/go/v1/usage" 2>/dev/null || true)"

if [ "$http_status" = "401" ] || [ "$http_status" = "403" ]; then
local message
message="$(jq -r '.error.message // empty' "$tmp_body" 2>/dev/null || true)"
[ -n "$message" ] || message="OpenCode API key invalid (HTTP ${http_status})."
rm -f "$tmp_body"
json_error opencode opencode-api "${http_status}" provider "$message"
return 0
fi

if [ "$http_status" = "200" ] && jq -e '
def valid_window:
type == "object"
and (.usagePercent | type == "number" and . >= 0 and . <= 100)
and (.resetInSec | type == "number" and . >= 0);
(.rollingUsage | valid_window)
and (.weeklyUsage | valid_window)
and (.monthlyUsage | valid_window)
' "$tmp_body" >/dev/null 2>&1; then
jq -c '
def make_window($w; $minutes; $label): {
usedPercent: $w.usagePercent,
windowMinutes: $minutes,
resetsAt: ((now + $w.resetInSec) | todate),
resetDescription: $label
};
. as $body |
{
provider: "opencode",
source: "opencode-usage",
usage: {
identity: {providerID: "opencode", accountEmail: "OpenCode Go account", loginMethod: "api-key"},
accountEmail: "OpenCode Go account",
loginMethod: "api-key",
primary: make_window($body.rollingUsage; 300; "5h"),
secondary: make_window($body.weeklyUsage; 10080; "weekly"),
tertiary: make_window($body.monthlyUsage; 43200; "monthly"),
updatedAt: (now | todate)
},
credits: {
remaining: (if $body.useBalance == true
then "OpenCode Go · balance fallback enabled"
else "OpenCode Go"
end)
}
}
' "$tmp_body"
rm -f "$tmp_body"
return 0
fi

# /zen/go/v1/usage failed (timeout, 5xx, parse error). Try the documented
# /zen/go/v1/models endpoint — auth-only confirmation that the key works.
rm -f "$tmp_body"
tmp_body="$(mktemp)"
http_status="$(curl -sS --max-time 8 -o "$tmp_body" -w '%{http_code}' \
-H "Authorization: Bearer ${key}" \
-H "Accept: application/json" \
"https://opencode.ai/zen/go/v1/models" || true)"
rm -f "$tmp_body"

if [ "$http_status" = "200" ]; then
json_note_usage opencode opencode-models "OpenCode Go account" "api-key" \
"Authenticated — quota endpoint unavailable, usage at opencode.ai/zen" \
"OpenCode Go"
return 0
fi
json_note_usage opencode opencode-api "OpenCode Go account" "api-key" \
"API key set; OpenCode Zen unreachable" \
"OpenCode Go"
}

fetch_glm_native() {
# GLM and Z.ai share one credential family. Honor ZAI_API_KEY here too so a
# single key drives both cards (matches docs/providers.md credential matrix).
Expand Down Expand Up @@ -2441,7 +2548,7 @@ fetch_provider() {
json_note_usage cline cline-local "Cline account" "subscription" "No public quota API; usage is available in the Cline client" "app.cline.bot"
;;
opencode)
json_note_usage opencode opencode-local "OpenCode" "proxy" "OpenCode routes to underlying providers directly — check each provider's card" "opencode.ai"
fetch_opencode_native
;;
together)
fetch_together_native
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/opencode-error-401.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "type": "error", "error": { "type": "AuthError", "message": "Missing API key." } }
1 change: 1 addition & 0 deletions tests/fixtures/opencode-error-403.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "type": "error", "error": { "type": "EntitlementError", "message": "OpenCode Go subscription required." } }
1 change: 1 addition & 0 deletions tests/fixtures/opencode-models.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "object": "list", "data": [{"id": "claude-sonnet-5", "object": "model", "created": 1, "owned_by": "opencode", "name": "Claude Sonnet 5", "context_length": 1000000}] }
6 changes: 6 additions & 0 deletions tests/fixtures/opencode-usage-balance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"useBalance": true,
"rollingUsage": { "status": "ok", "resetInSec": 9000, "usagePercent": 42 },
"weeklyUsage": { "status": "ok", "resetInSec": 302400, "usagePercent": 18 },
"monthlyUsage": { "status": "ok", "resetInSec": 1209600, "usagePercent": 7 }
}
5 changes: 5 additions & 0 deletions tests/fixtures/opencode-usage-malformed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"useBalance": false,
"rollingUsage": { "status": "ok", "resetInSec": 9000, "usagePercent": 42 },
"weeklyUsage": { "status": "ok", "resetInSec": 302400, "usagePercent": 18 }
}
6 changes: 6 additions & 0 deletions tests/fixtures/opencode-usage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"useBalance": false,
"rollingUsage": { "status": "ok", "resetInSec": 9000, "usagePercent": 42 },
"weeklyUsage": { "status": "ok", "resetInSec": 302400, "usagePercent": 18 },
"monthlyUsage": { "status": "ok", "resetInSec": 1209600, "usagePercent": 7 }
}
Loading
Loading