Repository navigation
fix(ai): satisfy OpenCode Zen free-tier gate (403 FreeTierError) (#12306) - #12556
palprateek wants to merge 25 commits into
Conversation
…1357#12306) OpenCode Zen/Go gate the contributor free tier on client identity: a User-Agent whose leading token is opencode/<version> and an x-opencode-session matching ses_<12 hex><14 alnum>. omp's omp/* UA and raw session/install ids failed both checks, producing 403 FreeTierError on every free-tier model. applyInferenceHeaders now sends the canonical OpenCode UA and a stable ses_ token derived from the conversation session id (routing and prompt cache stay pinned across turns); conversation-less background traffic (usage polls, model discovery) attributes through the same formatter with the install id. The shared formatter lives in packages/catalog/src/wire/opencode.ts so catalog discovery and pi-ai send identical identity. Explicit caller UA overrides (Claude OAuth fingerprint) still win. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
The can1357#12306 gate is body-shaped as well as header-shaped: it requires stream:true and at least five OpenCode core tool names (bash, edit, glob, grep, read) in tools[], so tool-less auxiliary calls (advisors, one-shot helpers) were still refused with 403 FreeTierError after the client-identity fix. stream() now pads such requests with stub entries for the missing names via withOpenCodeGateTools — empty description, inert {type:"object"} schema, strict:false — wire-shape filler that never joins the caller's tool registry. Agent rosters that already carry the names pass through untouched (same array identity). When a denial does surface, isOpencodeFreeTierGateMessage classifies it as model-scoped client policy rather than a credential fault: the key keeps serving paid SKUs, so the 403 no longer rotates sibling credentials (isAuthRetryableError and stream()'s rotatable-403 arm) or reads as an auth failure, and formatMessage rewrites the raw 403 into an explanation that preserves the classifier markers. google-shared now passes provider into finalize() so the zen google-transport lane gets the same treatment. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
… rotation isDirectCredentialRotationError rotated sibling keys on any 403, but the OpenCode free-tier gate denial is model-scoped client policy: sibling keys fail identically and the credential keeps serving paid SKUs. Extends the same exclusion isAuthRetryableError already applies. Live-gateway verified: old identity 403s with FreeTierError; branch identity passes the gate (rate limiter answers instead). 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
Only two are needed: bash and read |
|
Addressed in 668b66c: the gate check was weakened from all five tool names to any two. |
roboomp
left a comment
There was a problem hiding this comment.
Thanks @palprateek — thorough, well-scoped fix for the OpenCode free-tier 403 with live A/B evidence, changelog entries in both touched packages, docs, and contract-level tests. Rank: P1 — the four-part fix (canonical opencode/* UA, deterministic ses_ session token via toOpenCodeSessionToken, tool-name body padding, non-rotating/model-scoped error classification) is correct and conventional; one behavioral tradeoff deserves a maintainer decision before merge.
Findings:
- stream.ts (should-fix / question):
withOpenCodeGateToolsadvertises real tool names (bash/edit/glob/grep/read) with empty descriptions on every OpenCode request, including tool-less auxiliary calls. Nothing setstoolChoice, so the model can emit a phantom tool call on an advisor/one-shot turn whose caller expects text and has no exec handler — yielding empty.text. Net still positive vs a hard 403, but worth confirmingtoolChoice: "none"(or equivalent) was considered, plus a test that auxiliary completions still return text. - google-shared.ts (nit): the added
providerarg tofinalizeis unrelated to #12306 (Google never matches the opencode/copilot/cline/ollama rewrites); harmless, just widens the diff.
Correctness spot-checks passed: applyInferenceHeaders preserves prior behavior for non-OpenCode/no-session paths; toOpenCodeSessionToken output satisfies OPENCODE_SESSION_TOKEN_PATTERN (26-char shape); Claude OAuth fingerprint still wins via setHeaderIfAbsent; padding funnels through streamDispatch for stream/complete/streamSimple. Minor doc drift: the stream.ts comment says the gate "requires at least five" tool names while the PR body says the gate now needs only two — padding all five is a safe superset, so no action needed.
Not anchored to diff
packages/ai/src/stream.ts:958— should-fix / maintainer question:withOpenCodeGateToolsadvertises real tool names (bash,edit,glob,grep,read) with empty descriptions on every OpenCode request, including tool-less auxiliary calls (advisors, one-shot helpers viastreamSimple/completeSimple). The stubs are wire-inert for the gate, but they are still valid tool declarations the model sees — nothing here setstoolChoice, so the model can emit abash/readtool call on an auxiliary turn whose caller has no exec handler and expects text. The result is anAssistantMessagewith a tool call and empty.text, i.e. a silently broken advisor/summary for OpenCode free models. Before padding, those calls hard-403; after, they mostly work but carry this new failure mode. Was forcingtoolChoice: "none"(or otherwise suppressing invocation) considered for the padded case? A test that a tool-less auxiliary completion still yields text (not a phantom tool call) would lock this down.
Addresses roboomp review on can1357#12556: stub gate tools must not become callable work on auxiliary turns that expect text.
|
Addressed the roboomp review in 8711894: padded tool-less OpenCode calls now pin |
|
@can1357 — ready for your gates: branch MERGEABLE at 6323af0, 0 unresolved threads, roboomp's findings addressed (toolChoice:none pin + google-shared rationale). Needs you: (1) approve the pending fork workflow runs (CI 35737735017, OMP Nix 35737735116 — action_required), (2) review approval. |
|
Rebased onto latest main (merge cf6fce4) — GitHub now reports MERGEABLE, 0 unresolved threads. Still needs a maintainer: (1) approve the fork workflow runs (action_required), (2) review approval — roboomp's stream.ts toolChoice question was fixed in 8711894 with wire tests. @can1357 |
Resolve inference-headers.ts: keep Vercel gateway attribution from main alongside the OpenCode free-tier gate identity (canonical UA + ses_ session token, background-traffic fallback).
|
Rebased onto latest main (merge 120995f) — GitHub now reports MERGEABLE, 0 unresolved threads. Still needs a maintainer: (1) approve the fork workflow runs (36322064606 CI, 36322064604 OMP Nix — |
Keep the strengthened usage-attribution test over main's deletion: aa5aab0 dropped the old existence-only assertions per the testing policy, but this branch's version asserts the exact gateway wire shape (canonical OPENCODE_USER_AGENT + OPENCODE_SESSION_TOKEN_PATTERN match) the free-tier gate enforces, so it defends an external contract rather than echoing internals.
|
Re-merged onto latest main (3b9375b) — MERGEABLE again, 0 unresolved threads. One resolution note: kept the strengthened usage-attribution test over aa5aab0's deletion — it now asserts the exact gateway wire shape the gate enforces, not the old existence checks. Suites green (102 pass), Still needs a maintainer: (1) approve fork runs (36546257086 CI, 36546257057 OMP Nix — |
commit 3b9375b Merge: 120995f d1932a6 Author: Prateek <palprateek2004@gmail.com> Date: Tue Sep 29 14:29:36 2026 +0530 Merge origin/main into fix/opencode-free-tier-headers Keep the strengthened usage-attribution test over main's deletion: aa5aab0 dropped the old existence-only assertions per the testing policy, but this branch's version asserts the exact gateway wire shape (canonical OPENCODE_USER_AGENT + OPENCODE_SESSION_TOKEN_PATTERN match) the free-tier gate enforces, so it defends an external contract rather than echoing internals. commit 120995f Merge: cf6fce4 7695ddb Author: Prateek <palprateek2004@gmail.com> Date: Sun Sep 27 18:50:20 2026 +0530 Merge origin/main into fix/opencode-free-tier-headers Resolve inference-headers.ts: keep Vercel gateway attribution from main alongside the OpenCode free-tier gate identity (canonical UA + ses_ session token, background-traffic fallback). commit cf6fce4 Merge: 6323af0 7ae76f8 Author: Prateek <palprateek2004@gmail.com> Date: Wed Sep 23 13:17:44 2026 +0530 Merge remote-tracking branch 'origin/main' into fix/opencode-free-tier-headers commit 6323af0 Merge: 8711894 0f9139f Author: Prateek <palprateek2004@gmail.com> Date: Tue Sep 22 19:34:04 2026 +0530 Merge remote-tracking branch 'origin/main' into fix/opencode-free-tier-headers commit 8711894 Author: Prateek <palprateek2004@gmail.com> Date: Tue Sep 22 19:31:27 2026 +0530 fix(ai): pin tool_choice none on padded tool-less OpenCode calls Addresses roboomp review on can1357#12556: stub gate tools must not become callable work on auxiliary turns that expect text. commit b45304b Merge: 668b66c 5e0fc86 Author: Prateek <palprateek2004@gmail.com> Date: Mon Sep 21 23:14:55 2026 +0530 Merge remote-tracking branch 'origin/main' into fix/opencode-free-tier-headers commit 668b66c Author: Prateek <palprateek2004@gmail.com> Date: Mon Sep 21 23:03:18 2026 +0530 fix(ai): pad OpenCode gate to two tool names (bash+read) commit 8f36df2 Merge: 2bcaf7b b0651dc Author: Prateek Pal <101188666+palprateek@users.noreply.github.com> Date: Sun Sep 20 23:27:03 2026 +0530 Merge branch 'can1357:main' into fix/opencode-free-tier-headers commit 2bcaf7b Author: Prateek <palprateek2004@gmail.com> Date: Sun Sep 20 03:03:59 2026 +0530 docs: add PR attribution to OpenCode gate changelog entries 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> commit 93ad4b5 Author: Prateek <palprateek2004@gmail.com> Date: Sun Sep 20 02:56:49 2026 +0530 fix(ai): exempt OpenCode free-tier gate denial from direct credential rotation isDirectCredentialRotationError rotated sibling keys on any 403, but the OpenCode free-tier gate denial is model-scoped client policy: sibling keys fail identically and the credential keeps serving paid SKUs. Extends the same exclusion isAuthRetryableError already applies. Live-gateway verified: old identity 403s with FreeTierError; branch identity passes the gate (rate limiter answers instead). 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> commit 713e154 Author: Prateek <palprateek2004@gmail.com> Date: Sun Sep 20 02:37:49 2026 +0530 fix(ai): satisfy OpenCode free-tier body gate and scope its 403 denial The can1357#12306 gate is body-shaped as well as header-shaped: it requires stream:true and at least five OpenCode core tool names (bash, edit, glob, grep, read) in tools[], so tool-less auxiliary calls (advisors, one-shot helpers) were still refused with 403 FreeTierError after the client-identity fix. stream() now pads such requests with stub entries for the missing names via withOpenCodeGateTools — empty description, inert {type:"object"} schema, strict:false — wire-shape filler that never joins the caller's tool registry. Agent rosters that already carry the names pass through untouched (same array identity). When a denial does surface, isOpencodeFreeTierGateMessage classifies it as model-scoped client policy rather than a credential fault: the key keeps serving paid SKUs, so the 403 no longer rotates sibling credentials (isAuthRetryableError and stream()'s rotatable-403 arm) or reads as an auth failure, and formatMessage rewrites the raw 403 into an explanation that preserves the classifier markers. google-shared now passes provider into finalize() so the zen google-transport lane gets the same treatment. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> commit b4142c9 Author: Prateek <palprateek2004@gmail.com> Date: Sun Sep 20 01:55:11 2026 +0530 fix(ai): send OpenCode client identity to satisfy free-tier gate (can1357#12306) OpenCode Zen/Go gate the contributor free tier on client identity: a User-Agent whose leading token is opencode/<version> and an x-opencode-session matching ses_<12 hex><14 alnum>. omp's omp/* UA and raw session/install ids failed both checks, producing 403 FreeTierError on every free-tier model. applyInferenceHeaders now sends the canonical OpenCode UA and a stable ses_ token derived from the conversation session id (routing and prompt cache stay pinned across turns); conversation-less background traffic (usage polls, model discovery) attributes through the same formatter with the install id. The shared formatter lives in packages/catalog/src/wire/opencode.ts so catalog discovery and pi-ai send identical identity. Explicit caller UA overrides (Claude OAuth fingerprint) still win. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
Docs: keep this branch's free-tier gating bullet; accept main's KDL-driven consolidation of the pre-existing whenThinking/big-pickle notes.
|
Re-merged onto latest main (b53c11c) — MERGEABLE, 0 unresolved threads. Docs resolution: kept this PR's free-tier gating bullet; accepted main's consolidation of the pre-existing Still needs a maintainer: (1) approve fork runs (37117180238 CI, 37117180217 OMP Nix — |
streamDispatch: layer the OpenCode gate-tool padding on top of main's withSupportedSamplingParams wrapper.
|
Re-merged onto latest main (9667937) — MERGEABLE, 0 unresolved threads. Resolution: gate-tool padding now layers on top of main's Still needs a maintainer: (1) approve fork runs (37123450011 CI, 37123450010 OMP Nix — |
commit 9667937 Merge: b53c11c d0cc523 Author: Prateek <palprateek2004@gmail.com> Date: Sat Oct 3 18:06:56 2026 +0530 Merge origin/main into fix/opencode-free-tier-headers streamDispatch: layer the OpenCode gate-tool padding on top of main's withSupportedSamplingParams wrapper. commit b53c11c Merge: 3b9375b 9348320 Author: Prateek <palprateek2004@gmail.com> Date: Sat Oct 3 16:10:00 2026 +0530 Merge origin/main into fix/opencode-free-tier-headers Docs: keep this branch's free-tier gating bullet; accept main's KDL-driven consolidation of the pre-existing whenThinking/big-pickle notes. commit 3b9375b Merge: 120995f d1932a6 Author: Prateek <palprateek2004@gmail.com> Date: Tue Sep 29 14:29:36 2026 +0530 Merge origin/main into fix/opencode-free-tier-headers Keep the strengthened usage-attribution test over main's deletion: aa5aab0 dropped the old existence-only assertions per the testing policy, but this branch's version asserts the exact gateway wire shape (canonical OPENCODE_USER_AGENT + OPENCODE_SESSION_TOKEN_PATTERN match) the free-tier gate enforces, so it defends an external contract rather than echoing internals. commit 120995f Merge: cf6fce4 7695ddb Author: Prateek <palprateek2004@gmail.com> Date: Sun Sep 27 18:50:20 2026 +0530 Merge origin/main into fix/opencode-free-tier-headers Resolve inference-headers.ts: keep Vercel gateway attribution from main alongside the OpenCode free-tier gate identity (canonical UA + ses_ session token, background-traffic fallback). commit cf6fce4 Merge: 6323af0 7ae76f8 Author: Prateek <palprateek2004@gmail.com> Date: Wed Sep 23 13:17:44 2026 +0530 Merge remote-tracking branch 'origin/main' into fix/opencode-free-tier-headers commit 6323af0 Merge: 8711894 0f9139f Author: Prateek <palprateek2004@gmail.com> Date: Tue Sep 22 19:34:04 2026 +0530 Merge remote-tracking branch 'origin/main' into fix/opencode-free-tier-headers commit 8711894 Author: Prateek <palprateek2004@gmail.com> Date: Tue Sep 22 19:31:27 2026 +0530 fix(ai): pin tool_choice none on padded tool-less OpenCode calls Addresses roboomp review on can1357#12556: stub gate tools must not become callable work on auxiliary turns that expect text. commit b45304b Merge: 668b66c 5e0fc86 Author: Prateek <palprateek2004@gmail.com> Date: Mon Sep 21 23:14:55 2026 +0530 Merge remote-tracking branch 'origin/main' into fix/opencode-free-tier-headers commit 668b66c Author: Prateek <palprateek2004@gmail.com> Date: Mon Sep 21 23:03:18 2026 +0530 fix(ai): pad OpenCode gate to two tool names (bash+read) commit 8f36df2 Merge: 2bcaf7b b0651dc Author: Prateek Pal <101188666+palprateek@users.noreply.github.com> Date: Sun Sep 20 23:27:03 2026 +0530 Merge branch 'can1357:main' into fix/opencode-free-tier-headers commit 2bcaf7b Author: Prateek <palprateek2004@gmail.com> Date: Sun Sep 20 03:03:59 2026 +0530 docs: add PR attribution to OpenCode gate changelog entries 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> commit 93ad4b5 Author: Prateek <palprateek2004@gmail.com> Date: Sun Sep 20 02:56:49 2026 +0530 fix(ai): exempt OpenCode free-tier gate denial from direct credential rotation isDirectCredentialRotationError rotated sibling keys on any 403, but the OpenCode free-tier gate denial is model-scoped client policy: sibling keys fail identically and the credential keeps serving paid SKUs. Extends the same exclusion isAuthRetryableError already applies. Live-gateway verified: old identity 403s with FreeTierError; branch identity passes the gate (rate limiter answers instead). 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> commit 713e154 Author: Prateek <palprateek2004@gmail.com> Date: Sun Sep 20 02:37:49 2026 +0530 fix(ai): satisfy OpenCode free-tier body gate and scope its 403 denial The can1357#12306 gate is body-shaped as well as header-shaped: it requires stream:true and at least five OpenCode core tool names (bash, edit, glob, grep, read) in tools[], so tool-less auxiliary calls (advisors, one-shot helpers) were still refused with 403 FreeTierError after the client-identity fix. stream() now pads such requests with stub entries for the missing names via withOpenCodeGateTools — empty description, inert {type:"object"} schema, strict:false — wire-shape filler that never joins the caller's tool registry. Agent rosters that already carry the names pass through untouched (same array identity). When a denial does surface, isOpencodeFreeTierGateMessage classifies it as model-scoped client policy rather than a credential fault: the key keeps serving paid SKUs, so the 403 no longer rotates sibling credentials (isAuthRetryableError and stream()'s rotatable-403 arm) or reads as an auth failure, and formatMessage rewrites the raw 403 into an explanation that preserves the classifier markers. google-shared now passes provider into finalize() so the zen google-transport lane gets the same treatment. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com> commit b4142c9 Author: Prateek <palprateek2004@gmail.com> Date: Sun Sep 20 01:55:11 2026 +0530 fix(ai): send OpenCode client identity to satisfy free-tier gate (can1357#12306) OpenCode Zen/Go gate the contributor free tier on client identity: a User-Agent whose leading token is opencode/<version> and an x-opencode-session matching ses_<12 hex><14 alnum>. omp's omp/* UA and raw session/install ids failed both checks, producing 403 FreeTierError on every free-tier model. applyInferenceHeaders now sends the canonical OpenCode UA and a stable ses_ token derived from the conversation session id (routing and prompt cache stay pinned across turns); conversation-less background traffic (usage polls, model discovery) attributes through the same formatter with the install id. The shared formatter lives in packages/catalog/src/wire/opencode.ts so catalog discovery and pi-ai send identical identity. Explicit caller UA overrides (Claude OAuth fingerprint) still win. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
stream.ts: keep gate padding; adopt main's in-flight limit plumbing and drop the now-unused $pickenv/providerEntries imports.
|
Re-merged onto latest main (fcdfa63) — MERGEABLE, 0 unresolved threads. Resolution: adopted main's in-flight-limit plumbing and import cleanup in Still needs a maintainer: (1) approve fork runs (37628087751 CI, 37628087677 OMP Nix — |
|
Warning: advisor: Advisor unavailable for opencode-zen/big-pickle: OpenCode rejected this request It seems the endpoint mandatorily needs read and bash tool. The adviser does not work with the latest commit and this pull request merged on top |
… names The default advisor roster (advise/read/grep/glob) carries three of the five known core names but no bash, and the gateway still answers 403 — the body gate mandatorily requires bash+read. Pad whichever of the pair is missing instead of counting any-two-of-five.
|
@NoelJacob — good catch, confirmed and fixed in d4938e2: the padding only fired below two-of-five, but your advisor roster carries |
|
Re-merged onto latest main (13c7e36) — MERGEABLE, 0 unresolved threads, clean auto-merge. Suites green (111 pass), Still needs a maintainer: (1) approve fork runs (38123553676 CI, 38123553639 OMP Nix — |
Port the withOpenCodeGateShape helper (contributed by NoelJacob): pad missing bash/read in streamSimpleRequest as well as streamDispatch, and keep the tool_choice:none pin off openai-responses, which 400s on anything but auto.
|
Reviewed and ported in 6a92d10 — thank you, all three parts were valuable: (1) your October partial-combo bisection is now cited in the wire comment as the evidence for the pair rule, (2) |
|
Yes it works |
|
@roboomp @can1357 — requesting re-review on the latest head (6f3276c, re-merged onto main; mergeability recomputing). Since your last look: (1) the gate rule is now the specific |
|
Despite all of this, today I hit issue again. Trying to figure what/why cause this. |
What
I asked for the complete header fix — the canonical
opencode/<version>User-Agent plus a stableses_-shaped session token — implemented on a branch in this repo with tests, and after reviewing the diff extended it to the body-gate (tool-name) half and the 403 error scoping.OpenCode's Zen/Go gateway refuses requests that don't carry OpenCode's own client
identity. All four gate conditions were verified against the live gateway:
User-Agentleading tokenopencode/<version>— omp now sendsopencode/1.18.31(explicit caller overrides like the Claude OAuth fingerprint still win)
x-opencode-sessionmatchingses_<12 hex><14 alnum>— now a deterministic tokenderived from the conversation id, so routing and prompt-cache pinning survive
across turns; background traffic (usage polls, discovery) derives from the install id
stream: true— omp's agent turns already streambash+readtool names intools[](any-two-of-five does notsuffice — the default advisor roster
read/grep/globwithoutbashis still refused) —
stream()(single funnel behindstream/complete/streamSimple) pads calls missing either name (advisors,one-shot helpers) with stub entries for the missing pair member; stubs are wire-inert
and never enter the tool registry
Also scoped the error: a
403 FreeTierErrordenial is model-scoped client policy, nota credential problem — it no longer rotates sibling keys or invalidates the stored
credential, and the surfaced message explains the restriction instead of reading like
an auth failure. Shared contract lives in
@oh-my-pi/pi-catalog/wire/opencode.Why
fixes #12306 — community bisection there established all four conditions; two open PRs
(#12325, #12326) each implemented only conditions 1–2, which the bisection shows is
insufficient (tool-less auxiliary calls keep 403ing).
Testing
bun checkclean; pi-ai 4863 pass, pi-catalog 1027/1027 (the 12 pre-existingpi-ai env failures reproduce on clean main)
Live gateway A/B (byte-identical bodies, only identity headers varied):
omp/18.2.6+ raw UUID session)403 FreeTierErroropencode/1.18.31+ses_token)429 FreeUsageLimitError(gate passed; quota limiter answered)ses_session403 FreeTierError(UA independently required)402 Insufficient funds(gate only guards free SKUs)A
429is the pass condition: the quota limiter only evaluates after the identitygate accepts. Wire capture confirmed padding: tool-less requests left the client as
bash,read.bun checkpasses