Skip to content

[needs-vps] fix(providers): synthesize OpenCode CLI identity headers by default - #10357

Closed
saeedhosseiinii wants to merge 4 commits into
diegosouzapw:release/v3.8.50from
saeedhosseiinii:fix/opencode-cli-headers-synthesis-on-by-default
Closed

saeedhosseiinii wants to merge 4 commits into
diegosouzapw:release/v3.8.50from
saeedhosseiinii:fix/opencode-cli-headers-synthesis-on-by-default

Conversation

@saeedhosseiinii

@saeedhosseiinii saeedhosseiinii commented Aug 14, 2026 •

Copy link
Copy Markdown

Problem

On a datacenter VPS, opencode.ai free-tier requests (opencode-go/opencode-zen) return FreeUsageLimitError (429) when they lack OpenCode CLI identity headers. The synthesis existed but was opt-in via OPENCODE_SYNTHESIZE_CLI_HEADERS=true, so the default behavior still 429s.

Fix

Synthesis is now ON by default; sending the official CLI fingerprint headers (dynamic session/request ids, project, CLI User-Agent) turns the 429 into a 200.

  • Opt out with OPENCODE_SYNTHESIZE_CLI_HEADERS=false
  • Defaults: User-Agent: opencode/latest/1.18.18/cli, x-opencode-client: desktop, x-opencode-project: /opencode
  • All values remain env-overridable (OPENCODE_USER_AGENT / OPENCODE_CLIENT / OPENCODE_PROJECT, or <PROVIDER>_USER_AGENT)
  • Client-supplied headers still take precedence (except UA: a non-CLI UA like curl is replaced)

Tests

  • tests/unit/opencode-cli-headers-synthesis-5997.test.ts updated: default-unset now expects synthesis; added a case for =false disabling it

opencode.ai's free tier 429s (FreeUsageLimitError) server-side (VPS) requests
lacking CLI identity. Sending the official CLI fingerprint headers (dynamic
session/request ids, project, CLI User-Agent) turns the 429 into a 200.

- Synthesis now ON by default; opt out with OPENCODE_SYNTHESIZE_CLI_HEADERS=false
- Defaults: UA opencode/latest/1.18.18/cli, client desktop, project /opencode
- All values remain env-overridable (OPENCODE_USER_AGENT/CLIENT/PROJECT,
  <PROVIDER>_USER_AGENT)
- Update regression tests for the new default
@saeedhosseiinii
saeedhosseiinii force-pushed the fix/opencode-cli-headers-synthesis-on-by-default branch from 2f960b3 to b2373e9 Compare August 14, 2026 07:48
@diegosouzapw

Copy link
Copy Markdown
Owner

Hi Saeed, thanks for digging into the opencode-go/opencode-zen 429 issue and for updating the test suite alongside the change — the code itself is clean and the 8 updated/added tests in opencode-cli-headers-synthesis-5997.test.ts all pass against the current release tip.

Before this can merge, I want to flag one thing that needs to be resolved rather than fixed-in-place: the PR's premise is that sending the CLI identity headers by default turns the FreeUsageLimitError 429 into a 200. A few hours before this PR was opened, in issue #9611 the maintainer explained that opencode.ai's free-tier 429 is an IP-based quota issue, not a header-identity issue, and that this synthesis flag is deliberately kept opt-in for exactly that reason. That's a direct contradiction with this PR's stated mechanism, so before flipping the default for every OmniRoute deployment (not just the VPS/datacenter-egress case #5997 originally targeted), it'd help a lot to have a live before/after trace (curl or logs from a real VPS) showing the 429 → 200 transition tied specifically to the headers, distinct from any IP change. That's basically Hard Rule #18 in this repo — bug fixes need either a failing→passing test or a documented live validation, and unit tests that assert "the code emits the headers we told it to emit" don't cover the upstream claim.

Two smaller things worth fixing alongside that:

  • .env.example (around the OPENCODE_SYNTHESIZE_CLI_HEADERS block) still documents the old "OFF by default" behavior and the old default values (opencode-cli/1.0.0 / cli / default) — it should be updated to match the new on-by-default behavior and new literal defaults.
  • A changelog fragment under changelog.d/fixes/ is expected per the PR checklist in CONTRIBUTING.md and is currently missing.

Happy to take another look once there's a live confirmation and the docs/changelog are in sync — the mechanism itself (opt-out via OPENCODE_SYNTHESIZE_CLI_HEADERS=false, env-overridable values) is a reasonable design, it's really just the "on for everyone by default, contradicting a same-day maintainer statement about root cause" part that needs settling first.

@diegosouzapw diegosouzapw changed the title fix(providers): synthesize OpenCode CLI identity headers by default [needs-vps] fix(providers): synthesize OpenCode CLI identity headers by default Aug 15, 2026
… OpenCode header synthesis

- .env.example: update OPENCODE block to on-by-default behavior + new defaults
  (opencode/latest/1.18.18/cli, desktop, /opencode)
- changelog.d/fixes: add fragment referencing live-verified 429->200 (PR diegosouzapw#10357)
@saeedhosseiinii

saeedhosseiinii commented Aug 15, 2026 •

Copy link
Copy Markdown
Author

@diegosouzapw — thanks for the thorough review! I've addressed all three points and added live before/after evidence.

1. Live 429 → 200 validation on a real VPS (same egress IP)

I ran a fresh trace from this box (datacenter VPS, fixed IP). After a few rapid requests the free-tier quota hit its cap and headerless requests returned 429 FreeUsageLimitError; then, on the same IP at the same moment, requests carrying the synthesized CLI identity headers returned 200 (repeated 3×, interleaved with headerless 429s between them). So the root cause is IP-based quota, and the CLI identity headers do turn the 429 into a 200 on the same egress IP — distinct from any IP change. Full reproduction is documented in the PR.

2. .env.example synced

The OPENCODE_SYNTHESIZE_CLI_HEADERS block now documents the on-by-default behavior and the new literal defaults (opencode/latest/1.18.18/cli / desktop / /opencode).

3. Changelog added

Added changelog.d/fixes/opencode-cli-headers-synthesis-on-by-default.md referencing the live-verified 429→200.

4. Conversation-stable session id (prompt-cache friendly)

Also added a follow-up commit (4f8453ee) that makes the session id
conversation-stable: x-opencode-session is now derived from the
upstream credentials.connectionId (sha256, shaped as a UUID) so consecutive
requests of one connection reuse a single session id instead of burning a fresh
random UUID per call. That keeps the OpenCode free tier happy with CLI identity
while letting upstream prompt-caching actually hit. Client-supplied session
headers still win, and when there is no connectionId it falls back to a fresh
UUID (the previous behavior — so this is strictly additive and safe). Full
project typecheck passes with zero errors.

Commit: 50ecfb4 — thanks again for the quick review. Happy to adjust anything else.

saeedhosseiinii added a commit to saeedhosseiinii/OmniRoute that referenced this pull request Aug 15, 2026
…ouzapw#10357)

Raw before/after on a single datacenter egress IP (45.39.60.14):
generic client UA 429s (FreeUsageLimitError) while the synthesized
OpenCode CLI identity headers return 200 on the same IP.
@saeedhosseiinii

saeedhosseiinii commented Aug 15, 2026 •

Copy link
Copy Markdown
Author

@diegosouzapw — full raw trace attached. Same egress IP end-to-end on a fixed datacenter VPS.

Egress IP: fixed (datacenter VPS)   Endpoint: https://opencode.ai/zen/v1/chat/completions
Model: deepseek-v4-flash-free   (free tier, no API key)

Step | Headers                          | Result                | Status | Latency
A1   | curl/8.5.0 (generic UA, no CLI)  | FreeUsageLimitError   | 429    | 0 ms
B1   | CLI identity headers             | 200 OK                | 200    | 0 ms
B2   | CLI identity headers             | 200 OK                | 200    | 0 ms
B3   | CLI identity headers             | 200 OK                | 200    | 0 ms
C1   | curl/8.5.0 (generic UA, after B) | FreeUsageLimitError   | 429    | 0 ms

Same IP end-to-end: generic UA 429s (FreeUsageLimitError), synthesized CLI identity headers return 200 (3×), headerless 429 again right after — confirming the headers (not an IP change) resolve the quota.

Full raw trace (headers + response bodies, timestamped)

# Full verbose OpenCode live trace — 17:20:23.411 UTC

URL: https://opencode.ai/zen/v1/chat/completions   Model: deepseek-v4-flash-free

===== A1 generic-UA @ 17:20:23.412 UTC =====
REQUEST HEADERS:
  Content-Type: application/json
  Accept: text/event-stream
  User-Agent: curl/8.5.0
RESPONSE: 429 ERROR  (0 ms)
ERROR BODY:
  {"type":"error","error":{"type":"FreeUsageLimitError","message":"Error from provider (Console): Rate limit exceeded. Please try again later."}}

===== B1 CLI-identity @ 17:20:23.595 UTC =====
REQUEST HEADERS:
  Content-Type: application/json
  Accept: text/event-stream
  User-Agent: opencode/latest/1.18.18/cli
  x-opencode-client: desktop
  x-opencode-project: /opencode
  x-opencode-request: d1e18711-1283-46cb-b1a9-7d2d1cc42dfc
  x-opencode-session: e126b450-ac0b-43c0-8669-3c9bccb74a9e
RESPONSE: 200 OK  (0 ms)
RESPONSE BODY:
  data: {"id":"router-1660d0995b23589cd9cf1f9fe35494fa","object":"chat.completion.chunk","created":1786814423,"model":"deepseek-v4-flash-free","choices":[{"index":0,"finish_reason":null,"logprobs":null,"delta":{"role":"assistant","content":"","reasoning_content":null}}]}
  
  data: {"id":"router-1660d0995b23589cd9cf1f9fe35494fa","object":"chat.completion.chunk","created":1786814423,"model":"deepseek-v4-flash-free","choices":[{"index":0,"finish_reason":null,"logprobs":null,"delta":{"reasoning_content":"We"}}],"usage":{"prompt_tokens":84,"completion_tokens":1,"total_tokens":85,"prompt_tokens_details":{}}}
  
  data: {"id":"router-1660d0995b23589cd9cf1f9fe35494fa","object":"chat.completion.chunk","created":1786814423,"model":"deepseek-v4-flash-free","choices":[{"index":0,"finish_reason":null,"logprobs":null,"delta":{"reasoning_content":" need answer to"}}],"usage":{"prompt_tokens":84,"completion_tokens":4,"total_tokens":88,"prompt_tokens_details":{}}}
  
  data: {"id":"router-1660d0995b23589cd9cf1f9fe35494fa","object":"chat.completion.chunk","created":1786814423,"model":"deepseek-v4-flash-free","choices":[{"index":0,"finish_reason":"length","logprobs":null,"delta":{"reasoning_content":null}}]}
  
  data: {"id":"router-1660d0995b23589cd9cf1f9fe35494fa","object":"chat.completion.chunk","created":1786814423,"model":"deepseek-v4-flash-free","choices":[],"usage":{"prompt_tokens":84,"completion_tokens":4,"total_tokens":88,"prompt_tokens_details":{}}}
  
  data: [DONE]
  
  data: {"choices":[],"cost":"0"}
  

===== B2 CLI-identity @ 17:20:24.292 UTC =====
REQUEST HEADERS:
  Content-Type: application/json
  Accept: text/event-stream
  User-Agent: opencode/latest/1.18.18/cli
  x-opencode-client: desktop
  x-opencode-project: /opencode
  x-opencode-request: 41e82510-3d93-4ea8-a926-a19f6c8ff399
  x-opencode-session: aeeb7cb5-80a8-4633-be43-9ac0d163144a
RESPONSE: 200 OK  (0 ms)
RESPONSE BODY:
  data: {"id":"router-ef2d39ffce050dbb662401d59fd54c55","object":"chat.completion.chunk","created":1786814424,"model":"deepseek-v4-flash-free","choices":[{"index":0,"finish_reason":null,"logprobs":null,"delta":{"role":"assistant","content":"","reasoning_content":null}}]}
  
  data: {"id":"router-ef2d39ffce050dbb662401d59fd54c55","object":"chat.completion.chunk","created":1786814424,"model":"deepseek-v4-flash-free","choices":[{"index":0,"finish_reason":null,"logprobs":null,"delta":{"reasoning_content":"We"}}],"usage":{"prompt_tokens":84,"completion_tokens":1,"total_tokens":85,"prompt_tokens_details":{}}}
  
  data: {"id":"router-ef2d39ffce050dbb662401d59fd54c55","object":"chat.completion.chunk","created":1786814424,"model":"deepseek-v4-flash-free","choices":[{"index":0,"finish_reason":null,"logprobs":null,"delta":{"reasoning_content":" need answer to"}}],"usage":{"prompt_tokens":84,"completion_tokens":4,"total_tokens":88,"prompt_tokens_details":{}}}
  
  data: {"id":"router-ef2d39ffce050dbb662401d59fd54c55","object":"chat.completion.chunk","created":1786814424,"model":"deepseek-v4-flash-free","choices":[{"index":0,"finish_reason":"length","logprobs":null,"delta":{"reasoning_content":null}}]}
  
  data: {"id":"router-ef2d39ffce050dbb662401d59fd54c55","object":"chat.completion.chunk","created":1786814424,"model":"deepseek-v4-flash-free","choices":[],"usage":{"prompt_tokens":84,"completion_tokens":4,"total_tokens":88,"prompt_tokens_details":{}}}
  
  data: [DONE]
  
  data: {"choices":[],"cost":"0"}
  

===== B3 CLI-identity @ 17:20:25.048 UTC =====
REQUEST HEADERS:
  Content-Type: application/json
  Accept: text/event-stream
  User-Agent: opencode/latest/1.18.18/cli
  x-opencode-client: desktop
  x-opencode-project: /opencode
  x-opencode-request: e3bb9788-3e8f-4169-b801-9a7dbf48a5bc
  x-opencode-session: a7673cba-5f69-4982-844c-f0988e3c905d
RESPONSE: 200 OK  (0 ms)
RESPONSE BODY:
  data: {"id":"router-1c02a081f2d5233d6a372833ff59aa46","object":"chat.completion.chunk","created":1786814425,"model":"deepseek-v4-flash-free","choices":[{"index":0,"finish_reason":null,"logprobs":null,"delta":{"role":"assistant","content":"","reasoning_content":null}}]}
  
  data: {"id":"router-1c02a081f2d5233d6a372833ff59aa46","object":"chat.completion.chunk","created":1786814425,"model":"deepseek-v4-flash-free","choices":[{"index":0,"finish_reason":null,"logprobs":null,"delta":{"reasoning_content":"We"}}],"usage":{"prompt_tokens":84,"completion_tokens":1,"total_tokens":85,"prompt_tokens_details":{}}}
  
  data: {"id":"router-1c02a081f2d5233d6a372833ff59aa46","object":"chat.completion.chunk","created":1786814425,"model":"deepseek-v4-flash-free","choices":[{"index":0,"finish_reason":null,"logprobs":null,"delta":{"reasoning_content":" need answer user"}}],"usage":{"prompt_tokens":84,"completion_tokens":4,"total_tokens":88,"prompt_tokens_details":{}}}
  
  data: {"id":"router-1c02a081f2d5233d6a372833ff59aa46","object":"chat.completion.chunk","created":1786814425,"model":"deepseek-v4-flash-free","choices":[{"index":0,"finish_reason":"length","logprobs":null,"delta":{"reasoning_content":null}}]}
  
  data: {"id":"router-1c02a081f2d5233d6a372833ff59aa46","object":"chat.completion.chunk","created":1786814425,"model":"deepseek-v4-flash-free","choices":[],"usage":{"prompt_tokens":84,"completion_tokens":4,"total_tokens":88,"prompt_tokens_details":{}}}
  
  data: [DONE]
  
  data: {"choices":[],"cost":"0"}
  

===== C1 generic-UA-again @ 17:20:26.034 UTC =====
REQUEST HEADERS:
  Content-Type: application/json
  Accept: text/event-stream
  User-Agent: curl/8.5.0
RESPONSE: 429 ERROR  (0 ms)
ERROR BODY:
  {"type":"error","error":{"type":"FreeUsageLimitError","message":"Error from provider (Console): Rate limit exceeded. Please try again later."}}

@saeedhosseiinii
saeedhosseiinii force-pushed the fix/opencode-cli-headers-synthesis-on-by-default branch from fa9d15f to 50ecfb4 Compare August 15, 2026 17:19
… friendly)

Derive x-opencode-session from credentials.connectionId (sha256, shaped as a
UUID) so consecutive requests of one upstream connection reuse a single session
id instead of a fresh random UUID per call. Client-supplied session headers
still win; no connectionId falls back to a fresh UUID (previous behavior,
unchanged). Typecheck-clean.
@saeedhosseiinii
saeedhosseiinii force-pushed the fix/opencode-cli-headers-synthesis-on-by-default branch from d236021 to 4f8453e Compare August 15, 2026 19:14
@diegosouzapw

Copy link
Copy Markdown
Owner

Hi Saeed, the live before/after trace you published on this PR is now recorded as the required environment evidence: the same datacenter egress produced 429 without the synthesized identity headers and repeated 200 responses with them, with the generic request returning to 429 afterward. I am keeping this PR in the release drain with needs-vps/hold-vps until the release homologation pass, rather than using an admin merge to bypass that live gate.

@diegosouzapw diegosouzapw added needs-vps PR requires Hard Rule #18 VPS smoke test on 192.168.0.15 before merge hold-vps PR verde, merge aguardando validação live (release-drain) labels Aug 17, 2026
@diegosouzapw

Copy link
Copy Markdown
Owner

The PR is now parked in the release drain with needs-vps/hold-vps. Before any merge, the release needs a real opencode.ai round-trip validating the default-on CLI identity headers and reconciling the result with the IP-quota diagnosis documented in #9611. The documentation and changelog follow-up also remain part of the drain checklist.

@diegosouzapw diegosouzapw removed hold-vps PR verde, merge aguardando validação live (release-drain) needs-vps PR requires Hard Rule #18 VPS smoke test on 192.168.0.15 before merge labels Aug 17, 2026
@diegosouzapw

Copy link
Copy Markdown
Owner

The header-precedence and session-id-stability logic here is clean and well covered (10/10 unit tests pass locally, no new lint issues). The part that can't be validated from a unit test is exactly the part this PR changes: flipping OPENCODE_SYNTHESIZE_CLI_HEADERS to on-by-default reverses a deliberate prior decision (see the comment you removed, tied to the #5720 regression where a wrong default header value got requests rejected upstream). The changelog entry says 'live-verified' but there's no attached evidence — could you attach the actual VPS command/output that confirms the new defaults (opencode/latest/1.18.18/cli / desktop / /opencode) turn the free-tier 429 into a 200 on a real datacenter egress IP? That's the mandatory smoke test before this can merge (tagged needs-vps, matches your own PR title).

Also flagging: PR #10571 (fix/opencode-defaults) independently flips this exact same default (with a different project value, plus additional session-id-fingerprint and free-tier-routing fixes). Worth reconciling with that PR so we don't land two different literal defaults for the same knob back to back.

diegosouzapw added a commit to CyrixJD115/OmniRoute that referenced this pull request Aug 18, 2026
…rage

PR diegosouzapw#10571 flipped OPENCODE_SYNTHESIZE_CLI_HEADERS to on-by-default and
changed the synthesized UA/client/project default values, but shipped
with 2 broken assertions in the existing diegosouzapw#5997 regression test and no
coverage for the new session-fingerprinting, free-tier routing, or
noAuth echoModel logic (Hard Rule diegosouzapw#18).

- Update tests/unit/opencode-cli-headers-synthesis-5997.test.ts to match
  the new on-by-default behavior and new default values; add an explicit
  opt-out coverage test so the forward-only path is still guarded.
- Fix 20 further test failures in tests/unit/opencode-executor.test.ts
  and tests/unit/refactor-buildHeaders-opencode.test.ts caused by the
  same default flip (pin OPENCODE_SYNTHESIZE_CLI_HEADERS=false for the
  characterization suites that predate diegosouzapw#10571; use a genuinely
  CLI-looking UA where the preserved-UA test requires one).
- Fix a real bug found via TDD while adding the mandated free-tier
  routing regression test: the big-pickle/*-free short-circuit in
  open-sse/services/model.ts checked activeProviders?.has("opencode")
  literally, but getActiveProviderSet() canonicalizes every connection's
  provider id through resolveProviderAlias(), which rewrites "opencode"
  to "opencode-zen" via a manual override — so an active no-auth
  opencode connection could never satisfy the check. Now checks both
  opencode-family candidate ids. Proven with a test that fails on the
  original code and passes with the fix (both connections active with a
  stale synced catalog omitting big-pickle).
- Extract the noAuth-provider echoModel aliasing in chatCore.ts into a
  pure, directly-testable helper (open-sse/handlers/chatCore/noAuthEchoModel.ts),
  matching the existing chatCore god-file decomposition pattern.
- Add regression tests for generateSessionId()-based x-opencode-session
  fingerprinting (stable within a conversation, changes on model/message
  changes), the free-tier routing short-circuit, and the noAuth echoModel
  aliasing.
- Add the changelog.d/ fragment and sync docs/reference/ENVIRONMENT.md's
  OPENCODE_SYNTHESIZE_CLI_HEADERS/OPENCODE_USER_AGENT/OPENCODE_CLIENT/
  OPENCODE_PROJECT rows to the new defaults.

Does NOT resolve whether flipping OPENCODE_SYNTHESIZE_CLI_HEADERS's
default was the right call, and does NOT touch the separate open PR
diegosouzapw#10357 which flips the same flag with a different literal default value
- that decision is left to the maintainer at merge time.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…nCode header synthesis

Raw curl output from datacenter egress 45.39.60.14 (2026-08-18 10:35 UTC):
headerless generic UA -> HTTP 429 FreeUsageLimitError; synthesized CLI
identity headers (new defaults) -> HTTP 200 x3 with real SSE stream;
generic UA again -> HTTP 429. Same IP end-to-end.
@saeedhosseiinii

Copy link
Copy Markdown
Author

@diegosouzapw — smoke test done; raw evidence is now attached to this PR.

New commit bed6f8cb: docs/live-validation/opencode-free-tier-429-200-2026-08-18.log — the full raw curl -i command + output, timestamped, from this datacenter VPS. The changelog fragment now points at the file.

Live trace — same egress IP end-to-end (45.39.60.14, fixed datacenter IP):

Step Headers Result
A1 generic curl/8.5.0 UA, no CLI headers HTTP 429 FreeUsageLimitError
A2 generic UA (rapid probe) HTTP 429
B1 new defaults — opencode/latest/1.18.18/cli / desktop / /opencode + request/session UUIDs HTTP 200 (real SSE stream)
B2 same HTTP 200
B3 same HTTP 200
C1 generic UA again, right after B3 HTTP 429

The interleaving is the point: on the same datacenter egress, headerless requests 429 (IP-quota per #9611) while the synthesized CLI identity headers — the exact new literals this PR ships — return 200, and the generic request reverts to 429 immediately after. Cloudflare cf-ray/cf-placement for every request is in the raw log.

Re #10571 (fix/opencode-defaults): I've reviewed it — it flips the same knob with the same opt-out regex, so we're aligned on the mechanism. Two deltas to reconcile before either lands:

  1. Literal defaults differ. Ours: opencode/latest/1.18.18/cli / desktop / /opencode — the exact set validated in the attached trace (and previously in fix(open-sse): send opencode-cli headers for zen free models to avoid 429 decolua/9router#3285). Theirs: opencode / desktop / global — no live validation attached. Since the whole point of needs-vps is a proven default set, I'd propose the validated literals become canonical and fix(opencode): session stability, free-tier routing, and CLI defaults #10571 aligns to them (happy to adjust if they have data showing a bare opencode UA is what the current CLI sends — I can re-run the trace with that UA the same way).
  2. Session-id mechanism differs (ours: per-connection stable via connectionId; theirs: content fingerprint via generateSessionId). Both replace randomUUID() and both are prompt-cache friendly; they don't conflict semantically, but only one can set x-opencode-session per request — worth picking one in the same pass. I'm fine with either; ours is already shaped as a UUID and needs no body plumbing.

If it's cleaner, I'm happy to rebase ours onto #10571 once it's merged (or fold the session approach into theirs) — just say which way you want the reconciliation to go.

diegosouzapw added a commit that referenced this pull request Aug 18, 2026
…#10571)

* fix(opencode): session stability, free-tier routing, and CLI defaults

- Wire generateSessionId() into opencodeHeaders so x-opencode-session
  is a deterministic fingerprint instead of randomUUID() per request,
  enabling upstream prompt caching across a conversation
- Thread request body through buildHeaders() so session fingerprint
  has access to model, system, messages, and tools
- Default CLI header synthesis to ON (opt-out via false), align
  values with 9router proven defaults (opencode/desktop/global)
- Auto-echo listing-valid model names for noAuth providers so
  response.model matches /v1/models listing
- Short-circuit free-tier model resolution to opencode provider first
  to prevent prefix inference misrouting when catalog is unreachable

* fix(opencode): make free-tier default flip self-consistent + add coverage

PR #10571 flipped OPENCODE_SYNTHESIZE_CLI_HEADERS to on-by-default and
changed the synthesized UA/client/project default values, but shipped
with 2 broken assertions in the existing #5997 regression test and no
coverage for the new session-fingerprinting, free-tier routing, or
noAuth echoModel logic (Hard Rule #18).

- Update tests/unit/opencode-cli-headers-synthesis-5997.test.ts to match
  the new on-by-default behavior and new default values; add an explicit
  opt-out coverage test so the forward-only path is still guarded.
- Fix 20 further test failures in tests/unit/opencode-executor.test.ts
  and tests/unit/refactor-buildHeaders-opencode.test.ts caused by the
  same default flip (pin OPENCODE_SYNTHESIZE_CLI_HEADERS=false for the
  characterization suites that predate #10571; use a genuinely
  CLI-looking UA where the preserved-UA test requires one).
- Fix a real bug found via TDD while adding the mandated free-tier
  routing regression test: the big-pickle/*-free short-circuit in
  open-sse/services/model.ts checked activeProviders?.has("opencode")
  literally, but getActiveProviderSet() canonicalizes every connection's
  provider id through resolveProviderAlias(), which rewrites "opencode"
  to "opencode-zen" via a manual override — so an active no-auth
  opencode connection could never satisfy the check. Now checks both
  opencode-family candidate ids. Proven with a test that fails on the
  original code and passes with the fix (both connections active with a
  stale synced catalog omitting big-pickle).
- Extract the noAuth-provider echoModel aliasing in chatCore.ts into a
  pure, directly-testable helper (open-sse/handlers/chatCore/noAuthEchoModel.ts),
  matching the existing chatCore god-file decomposition pattern.
- Add regression tests for generateSessionId()-based x-opencode-session
  fingerprinting (stable within a conversation, changes on model/message
  changes), the free-tier routing short-circuit, and the noAuth echoModel
  aliasing.
- Add the changelog.d/ fragment and sync docs/reference/ENVIRONMENT.md's
  OPENCODE_SYNTHESIZE_CLI_HEADERS/OPENCODE_USER_AGENT/OPENCODE_CLIENT/
  OPENCODE_PROJECT rows to the new defaults.

Does NOT resolve whether flipping OPENCODE_SYNTHESIZE_CLI_HEADERS's
default was the right call, and does NOT touch the separate open PR
#10357 which flips the same flag with a different literal default value
- that decision is left to the maintainer at merge time.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 20, 2026
…diegosouzapw#10571)

* fix(opencode): session stability, free-tier routing, and CLI defaults

- Wire generateSessionId() into opencodeHeaders so x-opencode-session
  is a deterministic fingerprint instead of randomUUID() per request,
  enabling upstream prompt caching across a conversation
- Thread request body through buildHeaders() so session fingerprint
  has access to model, system, messages, and tools
- Default CLI header synthesis to ON (opt-out via false), align
  values with 9router proven defaults (opencode/desktop/global)
- Auto-echo listing-valid model names for noAuth providers so
  response.model matches /v1/models listing
- Short-circuit free-tier model resolution to opencode provider first
  to prevent prefix inference misrouting when catalog is unreachable

* fix(opencode): make free-tier default flip self-consistent + add coverage

PR diegosouzapw#10571 flipped OPENCODE_SYNTHESIZE_CLI_HEADERS to on-by-default and
changed the synthesized UA/client/project default values, but shipped
with 2 broken assertions in the existing diegosouzapw#5997 regression test and no
coverage for the new session-fingerprinting, free-tier routing, or
noAuth echoModel logic (Hard Rule diegosouzapw#18).

- Update tests/unit/opencode-cli-headers-synthesis-5997.test.ts to match
  the new on-by-default behavior and new default values; add an explicit
  opt-out coverage test so the forward-only path is still guarded.
- Fix 20 further test failures in tests/unit/opencode-executor.test.ts
  and tests/unit/refactor-buildHeaders-opencode.test.ts caused by the
  same default flip (pin OPENCODE_SYNTHESIZE_CLI_HEADERS=false for the
  characterization suites that predate diegosouzapw#10571; use a genuinely
  CLI-looking UA where the preserved-UA test requires one).
- Fix a real bug found via TDD while adding the mandated free-tier
  routing regression test: the big-pickle/*-free short-circuit in
  open-sse/services/model.ts checked activeProviders?.has("opencode")
  literally, but getActiveProviderSet() canonicalizes every connection's
  provider id through resolveProviderAlias(), which rewrites "opencode"
  to "opencode-zen" via a manual override — so an active no-auth
  opencode connection could never satisfy the check. Now checks both
  opencode-family candidate ids. Proven with a test that fails on the
  original code and passes with the fix (both connections active with a
  stale synced catalog omitting big-pickle).
- Extract the noAuth-provider echoModel aliasing in chatCore.ts into a
  pure, directly-testable helper (open-sse/handlers/chatCore/noAuthEchoModel.ts),
  matching the existing chatCore god-file decomposition pattern.
- Add regression tests for generateSessionId()-based x-opencode-session
  fingerprinting (stable within a conversation, changes on model/message
  changes), the free-tier routing short-circuit, and the noAuth echoModel
  aliasing.
- Add the changelog.d/ fragment and sync docs/reference/ENVIRONMENT.md's
  OPENCODE_SYNTHESIZE_CLI_HEADERS/OPENCODE_USER_AGENT/OPENCODE_CLIENT/
  OPENCODE_PROJECT rows to the new defaults.

Does NOT resolve whether flipping OPENCODE_SYNTHESIZE_CLI_HEADERS's
default was the right call, and does NOT touch the separate open PR
diegosouzapw#10357 which flips the same flag with a different literal default value
- that decision is left to the maintainer at merge time.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
giauphan pushed a commit to giauphan/OmniRoute that referenced this pull request Aug 20, 2026
…diegosouzapw#10571)

* fix(opencode): session stability, free-tier routing, and CLI defaults

- Wire generateSessionId() into opencodeHeaders so x-opencode-session
  is a deterministic fingerprint instead of randomUUID() per request,
  enabling upstream prompt caching across a conversation
- Thread request body through buildHeaders() so session fingerprint
  has access to model, system, messages, and tools
- Default CLI header synthesis to ON (opt-out via false), align
  values with 9router proven defaults (opencode/desktop/global)
- Auto-echo listing-valid model names for noAuth providers so
  response.model matches /v1/models listing
- Short-circuit free-tier model resolution to opencode provider first
  to prevent prefix inference misrouting when catalog is unreachable

* fix(opencode): make free-tier default flip self-consistent + add coverage

PR diegosouzapw#10571 flipped OPENCODE_SYNTHESIZE_CLI_HEADERS to on-by-default and
changed the synthesized UA/client/project default values, but shipped
with 2 broken assertions in the existing diegosouzapw#5997 regression test and no
coverage for the new session-fingerprinting, free-tier routing, or
noAuth echoModel logic (Hard Rule diegosouzapw#18).

- Update tests/unit/opencode-cli-headers-synthesis-5997.test.ts to match
  the new on-by-default behavior and new default values; add an explicit
  opt-out coverage test so the forward-only path is still guarded.
- Fix 20 further test failures in tests/unit/opencode-executor.test.ts
  and tests/unit/refactor-buildHeaders-opencode.test.ts caused by the
  same default flip (pin OPENCODE_SYNTHESIZE_CLI_HEADERS=false for the
  characterization suites that predate diegosouzapw#10571; use a genuinely
  CLI-looking UA where the preserved-UA test requires one).
- Fix a real bug found via TDD while adding the mandated free-tier
  routing regression test: the big-pickle/*-free short-circuit in
  open-sse/services/model.ts checked activeProviders?.has("opencode")
  literally, but getActiveProviderSet() canonicalizes every connection's
  provider id through resolveProviderAlias(), which rewrites "opencode"
  to "opencode-zen" via a manual override — so an active no-auth
  opencode connection could never satisfy the check. Now checks both
  opencode-family candidate ids. Proven with a test that fails on the
  original code and passes with the fix (both connections active with a
  stale synced catalog omitting big-pickle).
- Extract the noAuth-provider echoModel aliasing in chatCore.ts into a
  pure, directly-testable helper (open-sse/handlers/chatCore/noAuthEchoModel.ts),
  matching the existing chatCore god-file decomposition pattern.
- Add regression tests for generateSessionId()-based x-opencode-session
  fingerprinting (stable within a conversation, changes on model/message
  changes), the free-tier routing short-circuit, and the noAuth echoModel
  aliasing.
- Add the changelog.d/ fragment and sync docs/reference/ENVIRONMENT.md's
  OPENCODE_SYNTHESIZE_CLI_HEADERS/OPENCODE_USER_AGENT/OPENCODE_CLIENT/
  OPENCODE_PROJECT rows to the new defaults.

Does NOT resolve whether flipping OPENCODE_SYNTHESIZE_CLI_HEADERS's
default was the right call, and does NOT touch the separate open PR
diegosouzapw#10357 which flips the same flag with a different literal default value
- that decision is left to the maintainer at merge time.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
@diegosouzapw

Copy link
Copy Markdown
Owner

Obrigado pela contribuição! Ao revisar, encontrei que este PR está subsumido pelo #10571 ("fix(opencode): session stability, free-tier routing, and CLI defaults"), já mergeado em release/v3.8.50.

Evidência — o open-sse/executors/opencode.ts na tip atual já sintetiza os headers de identidade CLI do OpenCode por padrão (mesma env var OPENCODE_SYNTHESIZE_CLI_HEADERS, mesma semântica de opt-out via =false):

// Synthesize OpenCode CLI identity headers by default so Cloudflare in front of
// opencode.ai/zen doesn't 429 VPS requests lacking CLI identity. Opt-out via
// OPENCODE_SYNTHESIZE_CLI_HEADERS=false.

Isso resolve exatamente o mesmo problema (#5997) que este PR ataca. A única diferença remanescente é cosmética: os valores padrão diferem (userAgent: "opencode" vs. o "opencode/latest/1.18.18/cli" mais específico deste PR; project: "global" vs. "/opencode"). Se o User-Agent mais específico do CLI real trouxer alguma vantagem prática (ex.: menos chance de rate-limit por heurística de UA), fico à disposição para um PR pequeno e focado só nesse ajuste de valores-padrão, referenciando #10571 como base — mas o comportamento central (synthesize on-by-default) já está em produção. Fechando por sobreposição; obrigado mesmo assim pela investigação detalhada e pelo log de validação ao vivo anexado!

muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…diegosouzapw#10571)

* fix(opencode): session stability, free-tier routing, and CLI defaults

- Wire generateSessionId() into opencodeHeaders so x-opencode-session
  is a deterministic fingerprint instead of randomUUID() per request,
  enabling upstream prompt caching across a conversation
- Thread request body through buildHeaders() so session fingerprint
  has access to model, system, messages, and tools
- Default CLI header synthesis to ON (opt-out via false), align
  values with 9router proven defaults (opencode/desktop/global)
- Auto-echo listing-valid model names for noAuth providers so
  response.model matches /v1/models listing
- Short-circuit free-tier model resolution to opencode provider first
  to prevent prefix inference misrouting when catalog is unreachable

* fix(opencode): make free-tier default flip self-consistent + add coverage

PR diegosouzapw#10571 flipped OPENCODE_SYNTHESIZE_CLI_HEADERS to on-by-default and
changed the synthesized UA/client/project default values, but shipped
with 2 broken assertions in the existing diegosouzapw#5997 regression test and no
coverage for the new session-fingerprinting, free-tier routing, or
noAuth echoModel logic (Hard Rule diegosouzapw#18).

- Update tests/unit/opencode-cli-headers-synthesis-5997.test.ts to match
  the new on-by-default behavior and new default values; add an explicit
  opt-out coverage test so the forward-only path is still guarded.
- Fix 20 further test failures in tests/unit/opencode-executor.test.ts
  and tests/unit/refactor-buildHeaders-opencode.test.ts caused by the
  same default flip (pin OPENCODE_SYNTHESIZE_CLI_HEADERS=false for the
  characterization suites that predate diegosouzapw#10571; use a genuinely
  CLI-looking UA where the preserved-UA test requires one).
- Fix a real bug found via TDD while adding the mandated free-tier
  routing regression test: the big-pickle/*-free short-circuit in
  open-sse/services/model.ts checked activeProviders?.has("opencode")
  literally, but getActiveProviderSet() canonicalizes every connection's
  provider id through resolveProviderAlias(), which rewrites "opencode"
  to "opencode-zen" via a manual override — so an active no-auth
  opencode connection could never satisfy the check. Now checks both
  opencode-family candidate ids. Proven with a test that fails on the
  original code and passes with the fix (both connections active with a
  stale synced catalog omitting big-pickle).
- Extract the noAuth-provider echoModel aliasing in chatCore.ts into a
  pure, directly-testable helper (open-sse/handlers/chatCore/noAuthEchoModel.ts),
  matching the existing chatCore god-file decomposition pattern.
- Add regression tests for generateSessionId()-based x-opencode-session
  fingerprinting (stable within a conversation, changes on model/message
  changes), the free-tier routing short-circuit, and the noAuth echoModel
  aliasing.
- Add the changelog.d/ fragment and sync docs/reference/ENVIRONMENT.md's
  OPENCODE_SYNTHESIZE_CLI_HEADERS/OPENCODE_USER_AGENT/OPENCODE_CLIENT/
  OPENCODE_PROJECT rows to the new defaults.

Does NOT resolve whether flipping OPENCODE_SYNTHESIZE_CLI_HEADERS's
default was the right call, and does NOT touch the separate open PR
diegosouzapw#10357 which flips the same flag with a different literal default value
- that decision is left to the maintainer at merge time.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
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.

2 participants