You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Exhausted provider budgets are retried: 402 and structured quota codes are classified from message text only #47685
SessionRetry.retryable() decides whether a provider error is worth another attempt from prose:
the message and the response body are matched against RETRYABLE_MESSAGE_PATTERNS, plus two
zen-specific markers (FreeUsageLimitError, GoUsageLimitError).
A provider can report an exhausted budget without any of that — no recognisable phrase, and no long retry-after either. Those responses are classified as ordinary retryable errors, so the session
spends all five attempts on a limit that resets on a billing or daily window, and the real cause
never surfaces.
Concrete shapes this misses today:
HTTP 402 — payment required. Nothing about the status is momentary.
OpenAI billing codes in the body: insufficient_quota, billing_hard_limit_reached, billing_not_active, account_deactivated. These arrive as error.type / error.code on a 429
whose message ("You exceeded your current quota") matches the retryable patterns.
Google QuotaFailure with a per-day quota id, e.g. GenerateRequestsPerDayPerProjectPerModel, on a 429 whose message is "Resource has been
exhausted" — which the existing exhausted branch treats as provider overload.
These are documented, stable identifiers, unlike the message text, which varies per provider and
locale.
Related but different: #39790 covers the zen fixed-window quotas and the long-retry-after case,
which #47339 and #47641 address. Neither reaches a structural budget signal that comes with a short
or absent retry-after and no zen marker.
Use an OpenAI-compatible provider whose account has no remaining balance.
Send a prompt.
The provider answers 429 with insufficient_quota (or 402), the message matches the retryable
patterns, and the session retries five times instead of reporting the exhausted budget.
Thanks for the detailed report. I tested it against a mock OpenAI-compatible provider on @opencode/cli@2.2.0 and on the latest dev (b6c43a524), counting how many requests OpenCode sends for each error:
HTTP 402 and 429 with insufficient_quota are already handled on both. They're classified as an exhausted quota and not retried: the mock gets one attempt per call and the provider's message is shown. The other OpenAI billing codes you list usually come with a 400/401 status, which is already not retried.
Google per-day QuotaFailure still reproduces. A 429 with status: "RESOURCE_EXHAUSTED", the message "Resource has been exhausted", and a QuotaFailure detail whose quotaId is GenerateRequestsPerDayPerProjectPerModel is treated as an ordinary rate limit. The mock got 12 requests (each call tried six times) before the error appeared. The classifier only reads reason from error.details[] and never looks at violations[].quotaId.
The script below starts the mock provider and prints the request count for each case.
Reproduction script (repro.sh)
#!/usr/bin/env bash# Reproduce #47685: counts how many times OpenCode sends a request to a provider# that reports an exhausted budget. Usage: ./repro.sh [opencode command...]# Default command: `opencode` from PATH. For a source checkout:# PRELOAD=<repo>/packages/tui/node_modules/@opentui/solid/scripts/preload.js ./repro.sh bun --conditions=browser --preload=<repo>/packages/tui/node_modules/@opentui/solid/scripts/preload.js <repo>/packages/cli/src/index.tsset -u
CMD=("$@"); [ ${#CMD[@]}-eq 0 ] && CMD=(opencode)
DIR=$(mktemp -d);cd"$DIR"; git init -q
cat > server.ts <<'TS'const mode = process.env.MODE!let count = 0const bodies: Record<string, [number, unknown]> = { openai_insufficient_quota: [429, { error: { message: "You exceeded your current quota, please check your plan and billing details.", type: "insufficient_quota", code: "insufficient_quota" } }], http_402: [402, { error: { message: "Payment required" } }], google_per_day_quota: [429, { error: { code: 429, message: "Resource has been exhausted (e.g. check quota).", status: "RESOURCE_EXHAUSTED", details: [{ "@type": "type.googleapis.com/google.rpc.QuotaFailure", violations: [{ quotaMetric: "generativelanguage.googleapis.com/generate_requests_per_model_per_day", quotaId: "GenerateRequestsPerDayPerProjectPerModel" }] }] } }],}Bun.serve({ port: Number(process.env.PORT), fetch(req) { const u = new URL(req.url) if (u.pathname.endsWith("/chat/completions")) { count++; console.error(`REQUEST ${count} ${mode}`); const [s, b] = bodies[mode]; return Response.json(b, { status: s }) } if (u.pathname.endsWith("/models")) return Response.json({ data: [{ id: "m", object: "model" }] }) return new Response("nf", { status: 404 })} })TS
PORT=18765
# When running from source, spawned server processes need the OpenTUI preload too.
[ -n"${PRELOAD:-}" ] &&printf"preload = [\"%s\"]\n""$PRELOAD"> bunfig.toml
cat > opencode.json <<JSON{ "\$schema": "https://opencode.ai/config.json", "provider": { "mock": { "npm": "@ai-sdk/openai-compatible", "name": "Mock", "options": { "baseURL": "http://127.0.0.1:$PORT/v1", "apiKey": "x" }, "models": { "m": { "name": "m" } } } }, "model": "mock/m" }JSONforMODEin openai_insufficient_quota http_402 google_per_day_quota;do
MODE=$MODE PORT=$PORT bun server.ts 2> server.log & SP=$!; sleep 1
timeout 90 "${CMD[@]}" run --standalone --model mock/m "hi"> out.log 2>&1; RC=$?kill$SP;wait$SP2>/dev/null
echo"== $MODE: exit=$RC requests=$(grep -c REQUEST server.log) (timeout=124)"; tail -3 out.log | cut -c1-300
done
Activity
opencodejr commentedon Oct 28, 2026
Thanks for the detailed report. I tested it against a mock OpenAI-compatible provider on
@opencode/cli@2.2.0and on the latestdev(b6c43a524), counting how many requests OpenCode sends for each error:insufficient_quotaare already handled on both. They're classified as an exhausted quota and not retried: the mock gets one attempt per call and the provider's message is shown. The other OpenAI billing codes you list usually come with a 400/401 status, which is already not retried.QuotaFailurestill reproduces. A 429 withstatus: "RESOURCE_EXHAUSTED", the message "Resource has been exhausted", and aQuotaFailuredetail whosequotaIdisGenerateRequestsPerDayPerProjectPerModelis treated as an ordinary rate limit. The mock got 12 requests (each call tried six times) before the error appeared. The classifier only readsreasonfromerror.details[]and never looks atviolations[].quotaId.The script below starts the mock provider and prints the request count for each case.
Reproduction script (repro.sh)
Run it:
bash repro.sh