Skip to content

Fix OpenCode Free 403 FreeTierError: canonical CLI identity headers + live User-Agent - #14015

Closed
DangTAnh wants to merge 3 commits into
cheaper-inference:release/v3.8.51from
DangTAnh:fix/13935-opencode-403-freetiererror
Closed

DangTAnh wants to merge 3 commits into
cheaper-inference:release/v3.8.51from
DangTAnh:fix/13935-opencode-403-freetiererror

Conversation

@DangTAnh

@DangTAnh DangTAnh commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Part of #13935 — the header half. Body half (stream + tools contract) lives in #14013; land order between the two does not matter.

Summary

Keyless OpenCode requests failed with 403 FreeTierError because the synthesized CLI identity had rotted: an unversioned User-Agent pin plus random-UUID session/request ids. This PR brings the CLI identity (User-Agent + ses_/msg_ headers) up to the current upstream contract, with the CLI version resolved live so it cannot rot again. Scope is headers only — no request-body changes.

Problem

Requests through the OpenCode Free tier (oc/* models, e.g. the dashboard Playground) failed with:

403 FreeTierError: OpenCode's free tier can only be used from within OpenCode

The fallback UA was a hardcoded pin plus random-UUID session/request ids — upstream validates the CLI identity contract (opencode/<semver> + ses_[0-9a-f]{12}[base62]{14} / msg_…), so non-CLI clients kept getting rejected.

Changes

  • open-sse/utils/opencodeHeaders.ts (from fix(opencode): send canonical free-tier identity headers #13937, kept) — canonical ses_/msg_ generation per sst/opencode identifier.ts (descending for sessions, ascending for requests), deterministic SHA-256 translation of foreign session ids (conversation-stable, so upstream prompt-cache affinity holds), removal of the muse-spark UUID session overwrite. Client-supplied valid values always win.
  • open-sse/utils/opencodeCliVersion.ts (new) — resolves the live CLI version from the npm registry JSON API (opencode-ai/latest) via plain globalThis.fetch. No npm binary required, so npm-less and offline machines still work: cold/offline falls back to the pinned 1.18.31, and OPENCODE_USER_AGENT still overrides everything. Sync header synthesis never blocks — buildHeaders() reads cache/pin only, while execute() warms the cache fire-and-forget (coalesced, 6h TTL, never throws).
  • open-sse/executors/opencode.ts — execute() kicks the version refresh; buildHeaders() uses the live UA; sessionBody now also covers Responses-API input items so session fingerprints stay stable there. Also registers the oc alias on OpencodeExecutor so the no-auth Free tier gets the same synthesis.
  • .env.example — the opencode block documented stale defaults (opencode-cli/1.0.0 / cli / default, "OFF by default"); corrected to the shipped behavior (ON by default, live UA, desktop / global, canonical ids).
  • Tests — new opencode-cli-version.test.ts (5 tests: cold → pin without fetching, live registry read, offline keeps stale/pin, concurrent coalescing, malformed payloads); updated 5997 / 10571 / 12633 / executor suites to the canonical contract.

Verification

  • Unit: opencode suites 49/49 pass, opencode-executor 58/58 pass, tsc -p tsconfig.typecheck-core.json exit 0 (run 2026-09-18).
  • Live: headers verified byte-for-byte against the working OMP extension identity (opencode/1.18.31 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14 + canonical ses_/msg_).

Known limitation (the other half)

Headers alone do not satisfy the gate: measured live, a request also needs stream: true plus a non-empty tools array (and, for title/compaction-style calls that carry none, a borrowed/placeholder tool list) — that body half is #14013. This PR intentionally stops at headers so each half stays reviewable on its own.


AI-assisted with OMP (Oh My Pi).

@maxmad64bis

maxmad64bis commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Two things I measured on the current base (2387d051c1) that bear on this PR.

oc already reaches OpencodeExecutor. parseModel resolves the alias before getExecutor is called: resolveProviderAlias stops the chain on the registered no-auth id opencode (the #2901 boundary, open-sse/services/providerAlias.ts:38-58), and opencode is already in the lazy registry (open-sse/executors/index.ts:74). Running the real resolution on an untouched tree:

oc/big-pickle                      -> provider=opencode | executor=OpencodeExecutor
oc/muse-spark-1.3-contributor-free -> provider=opencode | executor=OpencodeExecutor

So on the chat-completions path the new oc entry is not reached, and the Playground uses that path — it builds oc/<model> and posts it like any other client. If some caller does hand getExecutor the raw prefix, naming it in the PR would help, since that caller is what the entry actually fixes.

The 403 in your "known limitation" is the request body rather than credential hydration. Measured across three models and both the Chat Completions and Responses surfaces, the upstream needs all four of: stream: true, a non-empty tools array, x-opencode-session shaped ses_ + 12 hex + 14 base62, and a User-Agent carrying opencode/<version >= 1.17>. Removing any single one turns a 200 back into a 403, and no header or credential change compensates. That fits what you observed: a direct execute() with the canonical identity still fails, because the body carries neither the streaming flag nor any tool. It also means a request that legitimately carries no tools — Playground, title generation, compaction — still answers 403 after this lands, so Closes #13935 looks optimistic.

#14013 adds that body half on top of the same header work from #13937, credited there. Between #13937, this PR and #14013 there are now three changes to open-sse/executors/opencode.ts and open-sse/utils/opencodeHeaders.ts; I am happy to rebase mine onto whichever one the maintainer prefers.

@DangTAnh
DangTAnh force-pushed the fix/13935-opencode-403-freetiererror branch from 4839353 to 720e50a Compare September 17, 2026 20:01
@DangTAnh DangTAnh changed the title Fix oc (OpenCode Free) 403 FreeTierError: route the no-auth alias to OpencodeExecutor + canonical CLI identity Fix OpenCode Free 403 FreeTierError: canonical CLI identity + live User-Agent Sep 17, 2026
@DangTAnh

Copy link
Copy Markdown
Contributor Author

Thanks for measuring both halves — verified on my side and updated the PR accordingly:

  1. oc entry dropped. You are right: parseModel("oc/...") resolves to provider id opencode before getExecutor is ever called, and opencode was already in the lazy registry. I grepped every getExecutor(/resolveExecutorWithProxy( caller — all pass the resolved id, never the raw oc prefix — so the entry was unreachable on the chat path, including the Playground. Removed it (amended + force-pushed); the golden snapshot is back to baseline. If you know a caller that does hand getExecutor the raw prefix, please point me at it and I will restore the entry with that caller named.

  2. Body contract: my numbers disagree, recorded honestly. With canonical headers + versioned UA, direct OpencodeExecutor("opencode") calls from this machine return 403 both with stream:true + non-empty tools and with stream:true alone — while the same model through the hydrated server path returns 200 with stream:false and no tools. So from my measurements the remaining gap looks like hydration/rotation state rather than the body, but I cannot rule out environment differences (egress IP, account state). I left the body half to fix(opencode): match the upstream free-tier request contract #14013, noted it in "Known limitation", and — as you said — happy to rebase onto whichever one the maintainer prefers.

…identity (cheaper-inference#13935)

- Register oc -> OpencodeExecutor("opencode"): the no-auth Free tier now gets Bearer public + canonical ses_/msg_ synthesis instead of DefaultExecutor pass-through (the Playground fix).

- Canonical ses_/msg_ generation per sst/opencode identifier.ts, deterministic foreign session translation, live CLI version from npm registry JSON API (no npm binary; pinned fallback offline).

- Correct stale .env.example opencode docs; regenerate executor-map golden snapshot.

- AI-assisted with OMP (Oh My Pi).
@DangTAnh
DangTAnh force-pushed the fix/13935-opencode-403-freetiererror branch from 720e50a to f5795b1 Compare September 17, 2026 20:41
- Default UA is now the exact working extension string (opencode/<live> ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14), not the bare version.

- Measured overnight: bare vs full UA both 403 from Node/Bun keyless, so this aligns us byte-for-byte with the only proven-working client pending the upstream/host fix; no behavior claim beyond parity.
@DangTAnh DangTAnh changed the title Fix OpenCode Free 403 FreeTierError: canonical CLI identity + live User-Agent Fix OpenCode Free 403 FreeTierError: canonical CLI identity headers + live User-Agent Sep 18, 2026
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thanks for this — the live CLI-version auto-refresh (npm registry fetch, 5s timeout, 6h TTL, fire-and-forget, offline-safe pin) is a genuinely nice idea. The #13935 fix itself has now merged via #14013 (headers + the measured body contract), so the header half of this PR is covered. Two notes on the rest: the oc: executor-registry entry is still present on this branch (open-sse/executors/index.ts) even though it was reported removed — resolveProviderAlias already maps oc → opencode before getExecutor runs, so it is dead code. And the auto-refresh, while nice, isn't required to fix the incident. Closing this one; if you'd like to land the auto-refresh on its own, a small standalone PR against release/v3.8.51 (without the oc: entry) would be very welcome.

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.

3 participants