What is done
src/quota.js (commit f6d1d33) parses provider rate-limit headers into a gated, one-line clause for the orchestrator, with tests whose fixtures are headers captured off the wire. Nothing feeds it — this issue is about the missing delivery seam.
Why it is worth finishing
Both providers report how much of their rate-limit window is burnt on every successful response, and opencode stores none of it. Today the orchestrator learns a provider is saturated only by taking a 429 mid-dispatch, after paying for the work so far. With the signal it would read, before dispatching:
[QUOTA] anthropic: 5h 83% (binding), 7d 6% used. The 5h window resets in 2h 25m.
Captured live 2026-09-01, HTTP 200 both:
POST api.anthropic.com/v1/messages
anthropic-ratelimit-unified-representative-claim five_hour
anthropic-ratelimit-unified-5h-utilization 0.07
anthropic-ratelimit-unified-5h-reset 1788258600
anthropic-ratelimit-unified-7d-utilization 0.06
POST chatgpt.com/backend-api/codex/responses
x-codex-primary-used-percent 51 x-codex-primary-window-minutes 300
x-codex-secondary-used-percent 15 x-codex-secondary-window-minutes 10080
x-codex-credits-has-credits True
Delivery paths, all measured
| Path |
Outcome |
Wrap globalThis.fetch |
Wrapper installed (patched=true, fetch not native, regex verified) and called for zero requests. ai-sdk only falls back to () => globalThis.fetch when options.fetch is unset, and the OAuth plugin already sets it. |
provider.<id>.options.fetch from the config hook |
The documented ai-sdk seam (new AY(H, {provider, url, headers, fetch: W.fetch, ...}) in the bundle), but at config time the OAuth plugin has not yet claimed the slot — wrapping finds it empty, takes it first, delegates to globalThis.fetch, and drops token injection. 240s hang with the call live vs 9.8s with it disabled. |
ctx.session.hook("http.response") |
Absent from opencode 1.18.23 ("http.response": 0 occurrences). Per upstream docs it would skip AI-SDK models anyway. |
ctx.aisdk.hook("language") |
The right seam — wrap the language model, read response.headers off doStream/doGenerate, transport untouched, auth unbreakable. Present in the binary (aisdk ×41, language ×16) but absent from @opencode-ai/plugin 1.17.8's typed surface: it belongs to the v2 Plugin.define({id, setup(ctx)}) runtime, and Plugin.define is 0 occurrences in this build. |
| Out-of-band dummy request |
No free endpoint carries the headers — /v1/models and /v1/messages/count_tokens (documented as not billed) both return 200 with no quota headers at all. And hand-rolled inference is client-gated: 429 rate_limit_error, generic "Error", no headers. Not pursued further. |
Also: api.type is "aisdk" or "native" at runtime, and is not in the typed Model (api: {id, url, npm}).
Plan
Wait for the v2 plugin API. When Plugin.define + ctx.aisdk.hook("language", ...) are reachable from a released @opencode-ai/plugin, delivery is roughly:
- wrap the model returned by the
language hook;
- read
response.headers from the doStream/doGenerate result;
parseQuotaHeaders(headers) → _quotaByProvider.set(providerID, snapshot);
- append
formatQuotaClause(...) to the existing [CACHE STATUS] note in handleTaskToolAfter;
- restore the
**Provider quota.** paragraph in skills/squad-delegate/SKILL.md (removed in f6d1d33 — it promised the orchestrator a line that cannot arrive).
A draft of the rejected options.fetch wrapper is not in tree; re-derive from this issue rather than reviving it.
Upstream
Six open issues ask for this and none is answered, labelled, or merged. anomalyco/opencode#42846 ("Plugins can't intercept outbound requests for AI-SDK models") is the exact blocker; #40588, #42765, #43324, #43983, #32423 are the same motivation from the user side. Worth watching #42846 rather than filing another.
Do not ship a globalThis.fetch monkey-patch as an interim: it is measured dead, not merely ugly.
What is done
src/quota.js(commit f6d1d33) parses provider rate-limit headers into a gated, one-line clause for the orchestrator, with tests whose fixtures are headers captured off the wire. Nothing feeds it — this issue is about the missing delivery seam.Why it is worth finishing
Both providers report how much of their rate-limit window is burnt on every successful response, and opencode stores none of it. Today the orchestrator learns a provider is saturated only by taking a 429 mid-dispatch, after paying for the work so far. With the signal it would read, before dispatching:
Captured live 2026-09-01, HTTP 200 both:
Delivery paths, all measured
globalThis.fetchpatched=true, fetch not native, regex verified) and called for zero requests. ai-sdk only falls back to() => globalThis.fetchwhenoptions.fetchis unset, and the OAuth plugin already sets it.provider.<id>.options.fetchfrom theconfighooknew AY(H, {provider, url, headers, fetch: W.fetch, ...})in the bundle), but atconfigtime the OAuth plugin has not yet claimed the slot — wrapping finds it empty, takes it first, delegates toglobalThis.fetch, and drops token injection. 240s hang with the call live vs 9.8s with it disabled.ctx.session.hook("http.response")"http.response": 0 occurrences). Per upstream docs it would skip AI-SDK models anyway.ctx.aisdk.hook("language")response.headersoffdoStream/doGenerate, transport untouched, auth unbreakable. Present in the binary (aisdk×41,language×16) but absent from@opencode-ai/plugin1.17.8's typed surface: it belongs to the v2Plugin.define({id, setup(ctx)})runtime, andPlugin.defineis 0 occurrences in this build./v1/modelsand/v1/messages/count_tokens(documented as not billed) both return 200 with no quota headers at all. And hand-rolled inference is client-gated:429 rate_limit_error, generic"Error", no headers. Not pursued further.Also:
api.typeis"aisdk"or"native"at runtime, and is not in the typedModel(api: {id, url, npm}).Plan
Wait for the v2 plugin API. When
Plugin.define+ctx.aisdk.hook("language", ...)are reachable from a released@opencode-ai/plugin, delivery is roughly:languagehook;response.headersfrom thedoStream/doGenerateresult;parseQuotaHeaders(headers)→_quotaByProvider.set(providerID, snapshot);formatQuotaClause(...)to the existing[CACHE STATUS]note inhandleTaskToolAfter;**Provider quota.**paragraph inskills/squad-delegate/SKILL.md(removed in f6d1d33 — it promised the orchestrator a line that cannot arrive).A draft of the rejected
options.fetchwrapper is not in tree; re-derive from this issue rather than reviving it.Upstream
Six open issues ask for this and none is answered, labelled, or merged.
anomalyco/opencode#42846("Plugins can't intercept outbound requests for AI-SDK models") is the exact blocker; #40588, #42765, #43324, #43983, #32423 are the same motivation from the user side. Worth watching #42846 rather than filing another.Do not ship a
globalThis.fetchmonkey-patch as an interim: it is measured dead, not merely ugly.