Skip to content

fix(providers): mint OpenCode free-tier sessions with the client's descending Identifier encoding - #16598

Merged
diegosouzapw merged 8 commits into
cheaper-inference:release/v3.8.52from
MRQC-st:fix/16328-opencode-free-tier-session
Oct 16, 2026
Merged

diegosouzapw merged 8 commits into
cheaper-inference:release/v3.8.52from
MRQC-st:fix/16328-opencode-free-tier-session

Conversation

@MRQC-st

@MRQC-st MRQC-st commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #16328.

What this changes

Standalone OmniRoute (no OpenCode CLI) returned 403 FreeTierError on 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 does

The upstream 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 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 with counter = 1 in the bit domain: ~((ts36 << 12) | 1n) & 0xFFFFFFFFFFFn. The low 12 hex therefore read ffe, and stay deterministic per conversation (prompt-cache affinity is preserved). This is byte-identical to opencode2api v1.3.8's descendingTimePart().
  • New OPENCODE_SESSION_TIME_PART_PATTERN = /^[0-9a-f]{9}f[c-f][0-9a-f]$/ and isValidOpencodeSessionValue(), 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_PATTERN alone 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 catalog

isPremiumOpencodeModel() 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:

signal source
available id ∈ the synced live catalog for the opencode family (catches delisted ids)
free models.dev cost.input === 0 && cost.output === 0 (catches big-pickle, which has no -free suffix)
not deprecated models.dev status !== "deprecated" (secondary guard)

Read synchronously from syncedAvailableModels + modelsDevSync, memoized on getModelCatalogCacheVersion() (bumped by every catalog write) — no per-request fetch, no table to rot. With no synced catalog it falls back to the legacy heuristic (-free suffix + 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 loose OPENCODE_SESSION_PATTERN assertions 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.json is clean. The full tests/unit/opencode-*.test.ts set 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:

POST https://opencode.ai/zen/v1/chat/completions
authorization: Bearer public
content-type: application/json
accept: */*
user-agent: opencode/1.19.0 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14
x-opencode-client: desktop
x-opencode-project: 374321b05e315c1177ca43fdc7e4bd79d702f37b
x-opencode-request: msg_<12hex + 14base62>   (fresh random per request)
x-opencode-session: ses_<12hex + 14base62>    (descending encoding, low 12 = 0xffe)

body: a captured client request (~138 KB) — model: "space-bunny-free", stream: true, ~82 KB system prompt, real tools array. 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.

…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.
@MRQC-st
MRQC-st requested a review from diegosouzapw as a code owner October 14, 2026 13:32
@MRQC-st

MRQC-st commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

Implemented the two-file scope from @opensource-elearning's review and opened it as a PR:

→ #16598 — open-sse/utils/opencodeHeaders.ts + open-sse/executors/opencodeFreeTierContract.ts

What landed (all three Ask-1 corrections + both Ask-2 corrections, no new shared module):

  • mint: canonicalId("ses_") 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 — byte-identical to opencode2api v1.3.8's descendingTimePart(), and deterministic per conversation so prompt-cache affinity survives.
  • value-level re-encode: isValidOpencodeSessionValue() checks shape and the 0xFC0–0xFFF window; applyCliDefaults() re-mints a shape-valid but out-of-window client session (opencode2api feat(docs,backup): export/import endpoints, documentation overhaul, screenshots #70's descendingTimePartPattern), and does so only on the gated zen path so opencode-go (fix(providers): OpenCode Go rewrites third-party User-Agent as OpenCode CLI #15311) keeps the client's session.
  • catalog-driven free set: dropped the static OPENCODE_FREE_MODELS; isPremiumOpencodeModel now reads id ∈ live /zen/v1/models ∧ models.dev cost.input === 0 && cost.output === 0 ∧ status !== "deprecated", memoized on getModelCatalogCacheVersion(). With no synced catalog it falls back to the legacy heuristic so an unsynced deploy isn't 402-ed wholesale.
  • tests tightened/added in opencode-free-tier-request-contract, opencode-background-identity-headers, opencode-premium-keyless-gate-8681.

On the un-reproduced host gate ("free tier can only be used from within OpenCode"): our 200s come from an APAC direct egress with Bearer public; the exact request envelope that passes is in the PR body. Decisive A/B on that envelope — ~(now<<12|1) → 200, epoch+1ms counter=1 → 200, counter=64 → 403 — is what makes the deterministic hash-derived mint safe.

MRQC-st and others added 3 commits October 15, 2026 10:01
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>
MRQC-st and others added 4 commits October 15, 2026 23:26
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
diegosouzapw merged commit 85c6914 into cheaper-inference:release/v3.8.52 Oct 16, 2026
3 checks passed
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.

fix(providers): OpenCode Free Tier 403 Errors When Used Standalone (without OpenCode CLI)

2 participants