Skip to content

fix(ai): satisfy OpenCode Zen free-tier gate (403 FreeTierError) (#12306) - #1

Merged
312022151125 merged 10 commits into
312022151125:mainfrom
palprateek:fix/opencode-free-tier-headers
Sep 23, 2026
Merged

312022151125 merged 10 commits into
312022151125:mainfrom
palprateek:fix/opencode-free-tier-headers

Conversation

@312022151125

Copy link
Copy Markdown
Owner

can1357#12556 (comment)

What

I asked for the complete header fix — the canonical opencode/<version> User-Agent plus a stable ses_-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:

  1. User-Agent leading token opencode/<version> — omp now sends opencode/1.18.31
    (explicit caller overrides like the Claude OAuth fingerprint still win)
  2. x-opencode-session matching ses_<12 hex><14 alnum> — now a deterministic token
    derived from the conversation id, so routing and prompt-cache pinning survive
    across turns; background traffic (usage polls, discovery) derives from the install id
  3. stream: true — omp's agent turns already stream
  4. ≥2 OpenCode core tool names in tools[] (weakened from five — bash +
    read suffice) — stream() (single funnel behind
    stream/complete/streamSimple) pads tool-less auxiliary calls (advisors,
    one-shot helpers) with stub entries for missing gate names; stubs are wire-inert
    and never enter the tool registry

Also scoped the error: a 403 FreeTierError denial is model-scoped client policy, not
a 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 can1357#12306 — community bisection there established all four conditions; two open PRs
(can1357#12325, can1357#12326) each implemented only conditions 1–2, which the bisection shows is
insufficient (tool-less auxiliary calls keep 403ing).

Testing

  • bun check clean; pi-ai 4863 pass, pi-catalog 1027/1027 (the 12 pre-existing
    pi-ai env failures reproduce on clean main)

  • Live gateway A/B (byte-identical bodies, only identity headers varied):

    Probe Identity Result
    free model old omp (omp/18.2.6 + raw UUID session) 403 FreeTierError
    free model this branch (opencode/1.18.31 + ses_ token) 429 FreeUsageLimitError (gate passed; quota limiter answered)
    isolation omp UA + correctly-shaped ses_ session 403 FreeTierError (UA independently required)
    paid SKU both identities 402 Insufficient funds (gate only guards free SKUs)

    A 429 is the pass condition: the quota limiter only evaluates after the identity
    gate accepts. Wire capture confirmed padding: tool-less requests left the client as
    bash,read.


  • bun check passes
  • Tested locally
  • CHANGELOG updated with the required attribution (if user-facing; internal issue fixes use issue links, external contributions add the PR link and contributor credit after creation)

palprateek and others added 10 commits September 20, 2026 01:55
…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>
Addresses roboomp review on can1357#12556: stub gate tools must not become
callable work on auxiliary turns that expect text.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@312022151125
312022151125 merged commit ab85e02 into 312022151125:main Sep 23, 2026
312022151125 pushed a commit that referenced this pull request Oct 2, 2026
composerChips() tested chip presence with text.includes(label); the
compact label "<icon> #1" is a substring of "<icon> can1357#10", so keeping
attachment can1357#10 also surfaced chip #1 in the attachment band.

- Added referencedAttachments(), which parses the buffer once with
  COMPOSER_TOKEN_REGEX and collects whole #<n> indices per kind.
- composerChips() now keys image, video, and paste chips on that set.

Supersedes can1357#11029, which targeted the pre-move coding-agent path.

Fixes can1357#13605
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

opencode-zen: muse-spark-1.3-contributor-free returns 403 FreeTierError in OMP while working in OpenCode

2 participants