Skip to content

fix(opencode): follow the published endpoint and the client's own headers - #16608

Closed
maxmad64bis wants to merge 1 commit into
cheaper-inference:release/v3.8.53from
maxmad64bis:fix/opencode-published-endpoint-client-headers
Closed

maxmad64bis wants to merge 1 commit into
cheaper-inference:release/v3.8.53from
maxmad64bis:fix/opencode-published-endpoint-client-headers

Conversation

@maxmad64bis

@maxmad64bis maxmad64bis commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ base-red inherited: #15306

Summary

Default changed: opencode and opencode-zen take their base URL from the api field of the model catalog the OpenCode client reads (now https://opencode.ai/inference/v1, where the pinned zen/v1 answers keyless requests with a 403), and a base URL set on the connection takes effect for them.

Requests from the OpenCode client go out with its own x-opencode-* headers and tool list as written, with Bearer public when there's no key, and a tool added on a shape retry carries the parameter schema the client last sent for it, since the upstream refuses a client tool name declared with an empty schema.

This restores the OpenCode client → OmniRoute → free model path, confirmed live on two free models with the client's headers; calls that don't carry the client's identity still get the 403.

Related Issues

Target Branch

  • Base is the active release/v3.8.x branch (the highest open release/v*), not main
  • New feature? From 3.8.55 on it is held with the v4-feature label for the v4 channel

Validation

  • Change type: provider
  • Focused tests and category gates from the golden path
  • npm run lint
  • Reconciled with the current active release base; focused checks rerun afterward
  • Production-code changes include a new or updated automated test in this PR

Tests Added Or Updated

  • tests/unit/opencode-published-endpoint.test.ts (29 cases): URL order and fallbacks, rejected catalog values, cold start, a catalog that stays down, header forwarding, auth forms; catalog and upstream stubbed.
  • tests/unit/opencode-client-request-shape.test.ts (8 cases): client requests sent as written, replay and its memory, borrowed schemas, bounded schema table, other clients unchanged; the stub applies the refusals measured on 2026-10-14.
  • Updated: existing opencode suites expect no x-opencode-request and Bearer on keyed Responses; two placeholder-schema assertions changed.

Coverage Notes

  • open-sse/utils/opencodePublishedEndpoint.ts, open-sse/executors/opencode.ts: opencode-published-endpoint and opencode-executor.
  • The contract, shape, observation and request-context modules and open-sse/utils/opencodeClient.ts: opencode-client-request-shape, opencode-free-tier-request-contract, opencode-request-shape-retry.
  • open-sse/utils/opencodeHeaders.ts, open-sse/handlers/chatCore/requestFormat.ts: refactor-opencodeHeaders, opencode-native-session-identity and the suites above.

Reviewer Notes

  • Behavior: URL order is the connection base URL, the catalog value (read every 6 hours and after a 403 FreeTierError), the last value saved under DATA_DIR/cache, then the registry. A cold start waits for one catalog read (5 s at most, once per 30 s when the catalog is down). The replay only follows a refusal and is remembered per prompt, so it doesn't add a round trip to every request. Live check: with this build behind a local gateway, the OpenCode 2.0.29 CLI got 200 from oc/ and opencode-zen for two free models, with and without an explicit title, from a cold start with no saved URL; no 403 in the call log.
  • Merge: conflicts with fix(opencode): declare the free-tier tool fingerprint the upstream requires #15322 in opencode.ts, and with fix(providers): remove OpenCode Free while preserving Zen and Go #16310 (on hold), which deletes opencodeFreeTierContract.ts; I'll rebase on whichever lands first.
  • Follow-up: call logs record the request body before the contract adds tools, and not the URL or headers sent, so a refusal's real request can't be read back; that's for a separate change. Each model's API surface still comes from the registry, not the published catalog and moves in a separate change; a replayed 429 still counts as a refusal in the shape memory; the 0.6–0.8 s keyless refusal before rotation stays to measure; the catalog value's source per attempt stays to log.

@maxmad64bis
maxmad64bis force-pushed the fix/opencode-published-endpoint-client-headers branch 3 times, most recently from 130c9ca to 7712656 Compare October 14, 2026 19:41
@maxmad64bis

Copy link
Copy Markdown
Contributor Author

If anyone wants to test it, please do. Feedback is welcome!
Ready once fully tested on a live installation.

@Theadd

Theadd commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Live installation validated: the agent→gateway bridge works — sustained 200s with real token throughput

Environment: self-hosted OmniRoute on Windows, built from the release/v3.8.52 tip with this PR cherry-picked (stacked on #16598 and #16541), plus a genuine OpenCode v2 agent (running inside the Orca ADE) pointed at the gateway, with a combo whose rungs are opencode / opencode-zen free models only. [VERIFIED]

What works

  • Sustained 200s through the bridge: e.g. longcat-2.5-preview-free completing in ~6s, muse-spark-1.3-contributor-free streaming 17–19s, usage rows with tens of thousands of input tokens (in=44919 out=1140, in=101236 out=319), cache hits reported.
  • Combo failover over the free models behaves exactly as this project wants it: a 403/429 on one rung rotates to the next; exhausted rungs stay in the combo and recover on their own.
  • The client-header forwarding this PR introduces is the decisive piece: the same accounts and the same gateway transport that refuse synthetic traffic return 200 once the request carries the genuine client's x-opencode-* headers, forwarded as sent.

What still refuses (expected — upstream gate by design)

  • Any request without the genuine client identity — synthetic replays, or internal gateway calls that don't ride an OpenCode client — gets 403 FreeTierError regardless of account credential (console-authenticated connections included: still 403). So this PR restores the agent→gateway→provider bridge, not anonymous gateway access.

Field notes for reviewers

  • This commit cherry-picks cleanly on top of fix(providers): mint OpenCode free-tier sessions with the client's descending Identifier encoding #16598 if the session-value mint is kept inside the renamed canonicalSessionId() helper — the two compose (descending session render + client-header forwarding) and all opencode suites pass together.
  • Combo operators beware: internal summary/handoff calls that hit a combo containing these free models without a client identity will hard-403; such rungs should stay out of non-agent internal call paths.
  • Minor latency tax observed: a no-auth attempt burning ~0.6–0.8s per request before rotation lands on the identity-carrying path — possibly worth a cooldown for this refusal class.

image

@maxmad64bis

maxmad64bis commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks a lot for the live test and the screenshot — that's the working control this needed.

On your points

  • Live validation: agreed on the scope, and the PR body now says it in those words: it restores the OpenCode client → gateway → free model path, and a call without the client's identity still gets the 403.
  • Composing with fix(providers): mint OpenCode free-tier sessions with the client's descending Identifier encoding #16598: yes, they're separate changes (session mint there, endpoint and client headers here). They touch the same files, so whichever lands second gets rebased; it's noted in the merge notes.
  • Internal combo calls without a client identity: expected, and this PR doesn't change it. Those calls can't pass the upstream check, so keeping free OpenCode rungs out of non-agent internal paths is the right call.
  • The ~0.6–0.8 s keyless refusal: in the screenshot each request first tries the noauth connection, gets a 403 with zero tokens, then lands on a keyed one. That's what happens when keyless requests still go to zen/v1, which now refuses keyless calls while a key still passes. The pause after a refusal is per provider and model, so a combo that walks several models pays one refusal per model every 3 minutes, which looks like a tax on every request. Could you check whether <DATA_DIR>/cache/opencode-published-endpoint.json exists on that host, and which URL the noauth attempt went to (request detail)? If the catalog read never succeeded there, keyless calls stayed on the old URL.

What changed in this push (rebased on the current release, still one commit)

  • Cold start waits for the catalog. Before, the first requests after a start went to the pinned zen/v1 until a background catalog read finished, so keyless calls got 403s; now a request with nothing in memory or on disk waits for one catalog read (5 s at most, once per 30 s if the catalog is down). The value is saved under DATA_DIR/cache and survives restarts. This is most likely what you're seeing.
  • The client's requests go out as written. A request from the OpenCode client is sent with its own tool list first, nothing added or removed; a title request, which the upstream only accepts without tools, no longer pays a refusal. A request refused for its shape is still replayed once in the other shape, and the shape that worked is remembered per prompt.
  • Added tools carry real schemas. When a retry has to add tools, each one now carries the parameter schema the client last sent for that name (an inert property if none was seen). The upstream refuses a client tool name declared with an empty schema, which is what the previous version sent.
  • Tests no longer depend on the machine. The suite no longer reads the real data directory, so a saved endpoint on the test host can't change its results.

@maxmad64bis
maxmad64bis force-pushed the fix/opencode-published-endpoint-client-headers branch 2 times, most recently from f1086d4 to e85072e Compare October 14, 2026 22:47
@maxmad64bis
maxmad64bis force-pushed the fix/opencode-published-endpoint-client-headers branch from e85072e to 05fd368 Compare October 15, 2026 17:20
@maxmad64bis
maxmad64bis marked this pull request as ready for review October 15, 2026 17:41
@maxmad64bis
maxmad64bis force-pushed the fix/opencode-published-endpoint-client-headers branch from 05fd368 to e769f96 Compare October 16, 2026 00:45
@maxmad64bis
maxmad64bis marked this pull request as draft October 16, 2026 11:07
@maxmad64bis
maxmad64bis force-pushed the fix/opencode-published-endpoint-client-headers branch from e769f96 to 8aefe8e Compare October 16, 2026 11:08
@maxmad64bis
maxmad64bis force-pushed the fix/opencode-published-endpoint-client-headers branch from 8aefe8e to 45cb935 Compare October 16, 2026 15:30
@maxmad64bis
maxmad64bis force-pushed the fix/opencode-published-endpoint-client-headers branch 2 times, most recently from c1ba17b to cd0cb10 Compare October 16, 2026 17:17
@maxmad64bis
maxmad64bis marked this pull request as ready for review October 16, 2026 17:54
@maxmad64bis
maxmad64bis marked this pull request as draft October 17, 2026 00:38
@maxmad64bis
maxmad64bis force-pushed the fix/opencode-published-endpoint-client-headers branch from cd0cb10 to 9c94197 Compare October 17, 2026 00:38
@diegosouzapw
diegosouzapw marked this pull request as ready for review October 17, 2026 02:14
…ders

Keyless requests went to the pinned zen/v1 and got 403, and tools added on a retry carried an empty schema the upstream refuses; the endpoint now follows the published catalog, the OpenCode client's requests and headers go out as it sent them, and each new case fails without the change (replaces the earlier head 7712656).
@maxmad64bis
maxmad64bis force-pushed the fix/opencode-published-endpoint-client-headers branch from 9c94197 to 990c410 Compare October 17, 2026 12:06
diegosouzapw added a commit to maxmad64bis/OmniRoute that referenced this pull request Oct 18, 2026
…hout cheaper-inference#16608's endpoint module

The cherry-picked surface map was filled by opencodePublishedEndpoint.ts, which
belongs to cheaper-inference#16608 (on hold) and is not on the release tip. The catalog module now
owns one coalesced in-memory read of models.opencode.ai/api.json (6 h TTL, 30 s
attempt floor, 5 s timeout, test-runner guard, no node: imports), and answers only
through a gate the executor registers (fail closed when absent or throwing). The
mappings for the removed keyless OpenCode Free provider (opencode / oc) are dropped;
Zen and Go keep their catalog sections.
diegosouzapw pushed a commit that referenced this pull request Oct 18, 2026
…ed catalog declares (#16798)

Merged via /merge-batch (2026-10-18, uplift round). Thanks @maxmad64bis for the idea and the original implementation (commit by Dizzle, carried with authorship as 2637fc7)!

This PR was stacked on #16608 (on HOLD: the keyless OpenCode Free path it restored was removed in #16310), so the branch was re-cut standalone off release/v3.8.52 with owner approval (force-push with lease): only this PR's own commit was cherry-picked, without #16608's endpoint module.

Maintainer rework on top:
- refactor(opencode): read the published model surfaces standalone (in-memory, 6 h cache, 30 s attempt floor, 5 s timeout, never throws, no src/ imports)
- feat(flags): add OPENCODE_PUBLISHED_CATALOG_ROUTING_ENABLED (default off)
- fix(opencode): gate the published-catalog surface lookup on that flag — with it off, getModelTargetFormat returns exactly the tip's answer and the catalog is never fetched

Evidence: validated on the combined board of the 34 reworked PRs, then on the current tip: focused tests 85/85 (opencode-model-surface-from-catalog 13/13 + flag registry), typecheck core/open-sse, boundary 774/774, cognitive-complexity within baseline, changelog-integrity, skills-sync Generated 0. Remaining CI reds are the inherited base-red #15306.
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thanks @maxmad64bis! Holding this one. We merged the current release/v3.8.52 into it: the four conflicts resolve cleanly, and 1,111 of 1,116 opencode tests pass. What blocks it is policy, not mechanics:

  • Keyless access (Authorization: Bearer public when there's no key) brings back the path that fix(providers): remove OpenCode Free while preserving Zen and Go #16310 removed on purpose. The keyless OpenCode Free provider was taken out honoring the OpenCode operator's public removal request; see docs/reference/REMOVED_PROVIDERS.md. Under our policy that path can't come back in any form, gated or not.
  • Header and schema contract: dropping the client's x-opencode-request and replacing {type:"object",properties:{}} with an inert property reverses contracts that fix(opencode): port v2 client fingerprint and tool contracts from 9router (#16571) #16998 pinned in tests/unit/opencode-v2-free-tier-fingerprint.test.ts. Five assertions there fail: lines 133, 170, 191 and 230, plus the real-header-path case.
  • Published-endpoint fetch: opencodePublishedEndpoint.ts duplicates the catalog fetch already on the base in opencodePublishedCatalog.ts (same URL, cache and timeouts). It's ungated and also writes a cache file under DATA_DIR/cache.

If the authenticated Zen and Go paths still need the published base URL or the client header passthrough, a reworked PR would help. It should cover only keyed requests, reuse opencodePublishedCatalog.ts, and sit behind a default-off flag so the #16998 contracts keep passing. We'd gladly review that.

@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.52 to release/v3.8.53 October 19, 2026 13:27
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Re-homed to release/v3.8.53: v3.8.52 entered its release freeze, so the branch now belongs to the release captain and development continues on the next cycle. Nothing is wrong with this PR — it just needed a live base. No action needed from you; CI will re-run against the new base.

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thank you for the thorough work and the live-test notes on this one. Since you opened it the OpenCode area has changed a lot on release/v3.8.53: the keyless OpenCode Free provider (opencode/oc) was removed in #16310 at the project owner's decision, the keyed free tier now goes to /inference/v1 with non-empty placeholder schemas (#16826), the v2 client fingerprint and tool contracts were ported (#16998), and published-catalog routing for key-based models landed in #16798. As a result this branch is now 253 commits behind and conflicts in opencode.ts, opencodeFreeTierContract.ts, opencodeHeaders.ts and the 12633 test, and the 'Bearer public on keyless requests' part would re-open what #16310 removed, so that needs an owner decision before anything else. If you still see a gap after those changes (published base-URL discovery for opencode-zen, or forwarding the client's own x-opencode-* headers), please open a fresh, narrow PR on the current tip with just that; from the earlier review, please also key the tool-schema memory per provider+session, route the catalog read through the proxy-aware fetch, and paste the 2026-10-14 measurement outputs into the body. I am holding this one rather than closing it so nothing is lost.

@maxmad64bis

Copy link
Copy Markdown
Contributor Author

Closing without merge, per the maintainer's guidance in the review above.

The keyless Bearer public path stays removed (#16310), so this branch cannot land as-is. The remaining useful parts move to a fresh, narrow PR on the current release/v3.8.53 tip: published base-URL discovery for the keyed opencode-zen path (reusing opencodePublishedCatalog.ts, proxy-aware fetch) and forwarding the client's own x-opencode-* headers — keyed requests only, flag off by default, with the 2026-10-14 measurement outputs in the body and tool-schema memory keyed per provider+session.

Thanks for the detailed review and for resolving the conflicts.

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