Repository navigation
fix(providers): mint OpenCode free-tier sessions with the client's descending Identifier encoding - #16598
Merged
diegosouzapw merged 8 commits intoOct 16, 2026
Conversation
…scending Identifier encoding
The Zen free tier decodes `x-opencode-session` back through OpenCode CLI's
`Identifier.descending()` layout — the 48-bit value `~(ts_ms << 12 | counter)`
in hex — and has refused any other encoding with 403 FreeTierError since
2026-10-13. The synthesizer hashed the seed into arbitrary hex, so its decoded
counter landed outside the accepted window every time.
- `canonicalId("ses_")` now takes the top 36 bits of the seed hash and inverts
with `counter = 1` in the bit domain, `~((ts36 << 12) | 1n) & 0xFFFFFFFFFFFn`,
so the low 12 hex read `ffe` and stay deterministic per conversation
(prompt-cache affinity is preserved). Byte-identical to opencode2api v1.3.8's
`descendingTimePart()`.
- New `isValidOpencodeSessionValue()` is the value-level check: the 12-hex time
part must decode to the client's counter window (0xFC0–0xFFF, counter 0–63).
`OPENCODE_SESSION_PATTERN` alone accepts any 12 hex, so a shape-valid but
out-of-window id still reached a 403. `applyCliDefaults()` now re-encodes on
the value, and only on the gated (zen) path — the ungated surfaces
(`opencode-go`, cheaper-inference#15311) keep the client's session unchanged.
Free-vs-premium no longer reads a static set (the old `OPENCODE_FREE_MODELS` had
already rotted: 5 of its 6 ids were absent from the live list, 4 deprecated). It
now reads the catalog OmniRoute already syncs — id available in `/zen/v1/models`
AND models.dev `cost.input === 0 && cost.output === 0` AND not deprecated —
memoized on `getModelCatalogCacheVersion()`. With no synced catalog it falls back
to the legacy heuristic, so an unsynced deployment keeps its behavior instead of
402-ing every free model.
Tests: opencode-free-tier-request-contract, opencode-background-identity-headers
and opencode-premium-keyless-gate-8681 cover the mint, the value-level
re-encode and the catalog-driven classification.
Fixes cheaper-inference#16328.
Contributor
Author
|
Implemented the two-file scope from @opensource-elearning's review and opened it as a PR: → #16598 — What landed (all three Ask-1 corrections + both Ask-2 corrections, no new shared module):
On the un-reproduced host gate ("free tier can only be used from within OpenCode"): our 200s come from an APAC direct egress with |
6 of 7 tasks
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…-gate suite Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…ncode-go keys Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
diegosouzapw
merged commit Oct 16, 2026
85c6914
into
cheaper-inference:release/v3.8.52
3 checks passed
4 tasks done
5 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #16328.
What this changes
Standalone OmniRoute (no OpenCode CLI) returned 403
FreeTierErroron every free-tier model. Two independent causes, two files, no new shared module — matching the scope agreed with @opensource-elearning.1.
open-sse/utils/opencodeHeaders.ts— mint the session the way the client doesThe upstream decodes
x-opencode-sessionback through OpenCode CLI'sIdentifier.descending()layout — the 48-bit value~(ts_ms << 12 | counter)in hex — and has refused any other encoding since 2026-10-13. The old synthesizer hashed the seed and emitted arbitrary hex, so its decoded counter landed out of the accepted window every time.canonicalId("ses_")now takes the top 36 bits of the seed hash and inverts withcounter = 1in the bit domain:~((ts36 << 12) | 1n) & 0xFFFFFFFFFFFn. The low 12 hex therefore readffe, and stay deterministic per conversation (prompt-cache affinity is preserved). This is byte-identical toopencode2apiv1.3.8'sdescendingTimePart().OPENCODE_SESSION_TIME_PART_PATTERN = /^[0-9a-f]{9}f[c-f][0-9a-f]$/andisValidOpencodeSessionValue(), the value-level check: the 12 hex time part must decode to the client's counter window (0xFC0–0xFFF, i.e. counter 0–63).OPENCODE_SESSION_PATTERNalone accepts any 12 hex, so a shape-valid but foreign-encoded id (raw hash, ascending id, counter ≥ 64) slipped through to a 403.applyCliDefaults()now re-encodes on the value, not just the shape: a genuine descending session is kept; everything else is re-minted from the client's own value. This is applied only on the gated (zen) path — the ungated surfaces (opencode-go, fix(providers): OpenCode Go rewrites third-party User-Agent as OpenCode CLI #15311) keep the client's session unchanged.2.
open-sse/executors/opencodeFreeTierContract.ts— derive free models from the synced catalogisPremiumOpencodeModel()no longer reads a static set (the oldOPENCODE_FREE_MODELShad already rotted: 5 of its 6 ids were absent from the live list, 4deprecated). It now reads the catalog OmniRoute already syncs:opencodefamily (catches delisted ids)cost.input === 0 && cost.output === 0(catchesbig-pickle, which has no-freesuffix)status !== "deprecated"(secondary guard)Read synchronously from
syncedAvailableModels+modelsDevSync, memoized ongetModelCatalogCacheVersion()(bumped by every catalog write) — no per-request fetch, no table to rot. With no synced catalog it falls back to the legacy heuristic (-freesuffix + the known ids) rather than treating everything as premium, so an unsynced deployment keeps its current behavior instead of 402-ing every free model.Tests
tests/unit/opencode-free-tier-request-contract.test.ts— tightened the looseOPENCODE_SESSION_PATTERNassertions to the value-level check; added cases for genuine-descending-preserved, out-of-window-re-minted, ungated-untouched, and catalog-driven classification.tests/unit/opencode-background-identity-headers.test.ts— same tightening.tests/unit/opencode-premium-keyless-gate-8681.test.ts— classification cases moved to the catalog seam.tsc -p tsconfig.typecheck-core.jsonis clean. The fulltests/unit/opencode-*.test.tsset shows no new failures versus the pre-change baseline (the ~12 remaining reds are pre-existing proxy/egress/rotation tests that fail in this local environment with or without the patch).On the un-reproduced 403
We still cannot reproduce the maintainer's "free tier can only be used from within OpenCode" on all free models from their host — our 200s come from an APAC direct egress. Exact envelope that returned 200:
body: a captured client request (~138 KB) —
model: "space-bunny-free",stream: true, ~82 KB system prompt, realtoolsarray. Decisive A/B (same envelope, only the session varies):~(now<<12|1)→ 200,~(1<<12|1)(epoch+1ms, counter=1) → 200,~(now<<12|64)(counter=64) → 403. The gate is the counter bit-pattern, and the timestamp is effectively unvalidated — which is what makes a deterministic, hash-derived mint safe here.