Skip to content

fix(opencode): replay a proxied 403 FreeTierError once on the direct egress (#4983) - #4988

Open
yxxrn wants to merge 1 commit into
decolua:masterfrom
yxxrn:fix/opencode-freetier-direct-replay
Open

yxxrn wants to merge 1 commit into
decolua:masterfrom
yxxrn:fix/opencode-freetier-direct-replay

Conversation

@yxxrn

@yxxrn yxxrn commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Problem

The OpenCode free-tier gate answers 403 FreeTierError when the request exits from an IP it has flagged. The transport itself succeeds, so the existing proxy-failure fallback in proxyAwareFetch never fires and every retry keeps leaving through the same flagged proxy pool — the lane stays 403 until a human rebinds the pool in the dashboard.

This is the failure mode users are hitting right now in #4983 (and the flip pattern reported in #4845): a configured pool whose egress IPs lose their reputation turns oc/* hard-403 with no automatic escape, while the identical request on the direct egress works.

Live measurement (same body, same headers, same minute, same machine)

egress result
flagged pool (zone-isp_proxy3), 3 runs 403 FreeTierError ×3
direct (native IP), 3 runs 200 ×3
healthy pool (zone-isp_proxy2, the one 9router-go uses), 3 runs 200 ×3
any egress, body without the read/shell fingerprint tools 403 (the second gate layer — not touched by this PR)

Because the 403 arrives on a successful proxied response, the current catch-based fallback (Proxy failed, falling back to direct) is structurally blind to it.

Change

open-sse/utils/proxyFetch.js:

  • After a proxied attempt returns 403, buffer the body and check for the FreeTierError marker. On a match, replay the request once on the direct egress (fetchWithTlsFallback(url, options, null)).
  • strictProxy: true keeps its contract: the proxied response is returned as-is, never a replay (checked before the body is even consumed).
  • A body already consumed by the first attempt (stream request bodies) cannot be resent — the original 403 is surfaced instead.
  • If the direct replay itself fails, the original 403 is surfaced (never a transport error replacing a readable upstream answer).
  • Non-gate 403s (any other error shape) are returned untouched with their original headers; streaming 2xx responses are never inspected — the new code only runs on status === 403.

Scope is intentionally narrow: the marker is OpenCode-specific, so no other provider can trigger the replay.

Verification

New tests/unit/opencode-freetier-direct-replay.test.js (5 cases):

  1. proxied 403 FreeTierError → one direct replay → returns its 200 (asserts call order: dispatcher present → absent).
  2. strictProxy: true → the 403 is returned, one fetch call, no replay.
  3. proxied 403 with a non-gate body → returned untouched (headers preserved), no replay.
  4. proxied 200 SSE stream → untouched, body not consumed, one call.
  5. direct replay throws → the original 403 body is surfaced.

Results:

  • This suite: 5/5 pass.
  • Regression check on the existing proxy tests touching this file (strict-proxy-enforcement, qoder-proxy-replay, cursor-agent-strict-proxy): identical 6 failed / 8 passed with and without the patch (those failures are the pre-existing environment/red-test baseline documented in CLAUDE.md — missing root deps such as uuid), i.e. 0 pass→fail.

Security

No header manipulation, no credential or token handling, no new network targets: the replay goes to the same URL the caller requested, over the direct egress path that proxyAwareFetch already falls back to for transport failures. strictProxy stays fail-closed.

Refs #4983.

…egress (decolua#4983)

The free-tier gate rejects the request when its egress IP is flagged, but the
transport itself succeeds - so the existing proxy-failure fallback never fires
and every retry keeps leaving through the same flagged proxy pool.

Live measurement, identical body and headers in the same minute: flagged pool
egress -> 403 FreeTierError, direct egress -> 200.

proxyAwareFetch now inspects a proxied 403 and, when the body carries the
FreeTierError marker, replays the request once on the direct egress.
strictProxy keeps its contract (never leaves the configured path), a body
already consumed cannot be resent, and a failed direct replay surfaces the
original 403. Non-gate 403s and streaming 2xx responses pass through
untouched.
@MarcusHO929

MarcusHO929 commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Withdrawn — correction after follow-up measurements (2026-10-20)

I withdraw my endorsement of #4987 as a root transport fix and the claim that this establishes a complementary DNS-plus-fallback solution. This correction concerns my comment only, not the author’s independent #4988 proposal.

Please do not treat the original text below as a current fix recommendation. I have withdrawn and am closing my local-DNS proposal #4987.

Follow-up measurements in this AdGuard setup showed a material side effect: with local DNS / numeric SOCKS targets, multiple pools could expose the same service-visible US egress; proxy-side DNS / domain targets exposed their respective distinct VPN egresses. We removed our locally hardcoded OpenCode DNS override and restored upstream 9router's original transport. The optional PR and the local hardcoded override were not the same implementation.

Earlier success/403 differences were time- and path-specific. They do not prove an upstream 9router DNS defect, a universal OpenCode fix, a Cloudflare WAF or load-shedding rule, or that our DNS override caused the IP-abuse flags. The exact server-side trigger and expiry remain unverified. Sorry for the earlier overstatement.

See #4987 for the full correction. No credentials or egress addresses are published here.

Superseded original comment — retained as history, not a current recommendation

Thanks for puting this together @yxxrn! This direct fallback is a great safety net when a proxy's egress IP genuinely loses its reputation or gets flagged by Cloudflare WAF.

I wanted to share our complementary findings from #4983 and #4987, as both PRs actually fit together really well:

Complementary findings on SOCKS proxies (#4987)

During our controlled tests on #4983 across 16 active SOCKS5 proxy pools, we found that many proxy rejections weren't caused by a permanently blacklisted proxy IP, but by SOCKS domain target addressing (remote DNS / ATYP 3):

  • Sending opencode.ai as a domain target through the SOCKS tunnel resulted in 403 FreeTierError across all 16 pools.
  • Resolving the destination address locally and sending the numeric IP through the exact same SOCKS proxy (while keeping original TLS SNI, HTTP Host, and certificate verification intact) turned all 16/16 pools back to 200 OK + complete streaming.
  • Even the genuine, unmodified OpenCode CLI reproduced this exact split (403 over SOCKS with remote DNS, 200 direct or with local DNS).

How #4987 and #4988 complement each other

  1. [Withdrawn] fix(proxy): add opt-in local DNS for SOCKS5 targets #4987 fixes the root transport path: With ?dns=local, SOCKS proxies avoid triggering the domain-routing 403 in the first place. This allows users who rely on proxy rotation or have strictProxy: true enabled to keep their traffic inside their configured proxy pools.
  2. fix(opencode): replay a proxied 403 FreeTierError once on the direct egress (#4983) #4988 provides the graceful fallback: When a proxy IP is genuinely blacklisted/rate-limited by Cloudflare WAF (like datacenter IPs or overused egresses), this replay mechanism automatically rescues the request to direct egress (for non-strict proxy users) instead of leaving the lane dead.

Having both—a proper transport option so proxies can succeed (#4987), plus a direct replay fallback when an egress is truly burnt (#4988)—gives users the best of both worlds.

leiblitz31 added a commit to leiblitz31/9router-go that referenced this pull request Oct 17, 2026
…t normalization, 403 direct replay

- internal/proxy/opencode.go: TranslateOpenCodeSessionID now generates fresh
  timestamped session IDs instead of hash-derived ones. The free-tier gate
  rejects non-recent timestamps with 403 FreeTierError.
- internal/translator/opencode_v2_fingerprint.go: port decolua/9router#4972 -
  normalize colliding fingerprint tool definitions in-place when arguments
  are compatible, instead of always aliasing to *_client.
- internal/proxy/proxy.go: port decolua/9router#4988 - replay proxied 403
  FreeTierError once on direct egress (unless strictProxy).
- internal/providers/capabilities.go: context window 1M for muse-spark and
  nemotron free models.
yorihaput added a commit to yorihaput/9router that referenced this pull request Oct 17, 2026
andyvandaric added a commit to andyvandaric/9router that referenced this pull request Oct 17, 2026
… strategy in full

Problem: the fork ran an older fingerprint contract than upstream frontier on the keyed lane and lacked both proxy escape hatches from issue decolua#4983. opencode-zen still sent the v1 UA plus a stub bash/glob/grep/read quartet with empty schemas and the v1-only x-opencode-request header, the fingerprint helper could not satisfy the gate when a caller tool reuses read/shell with platform-specific wording, and proxyAwareFetch had no way out of an egress the gate had flagged.

Changes: port upstream PR decolua#4972 (in-place fingerprint normalization with alias fallback), decolua#4883 (UA opencode/latest/2.0.29/cli across both executors, registry and dashboard test; strict v2 channel matcher kept), decolua#4860 fingerprint/strategy for opencode-zen (verbatim read/shell via applyFingerprintTools, cli client, four session mirrors, drop x-opencode-request and x-api-key for free tier; /zen/v1 to /inference/v1 switch NOT taken), decolua#4987 (open-sse/utils/proxyDispatcher.js with opt-in socks5 dns=local, shared by proxyFetch and the dashboard proxy test, fail-closed ERR_PROXY_DNS_MODE) and decolua#4988 (replay a proxied 403 FreeTierError once on the direct egress, strictProxy contract preserved). undici ^7.19.2 to ^7.30.1. Version 0.5.108.

Verification: full suite re-derived on this machine from a git stash -u baseline - failure sets byte-identical (95 = 95, 0 new), passes 3592 to 3617. Targeted opencode/proxy/session groups 214 pass. undici 7.30.1 exposes Socks5ProxyAgent and createSocks5Connection; the local-DNS dispatcher is exercised end to end against a local SOCKS5 plus TLS fixture (11 cases). Root cause of the free-tier 403 is egress-IP load-shedding (decolua#4983), so these ports restore request truth and add escape hatches but cannot make a flagged egress succeed.
mulqizamzam added a commit to mulqizamzam/9router that referenced this pull request Oct 18, 2026
KunN-21 added a commit to KunN-21/9router that referenced this pull request Oct 22, 2026
feat(opencode): integrate upstream decolua#4988 free-tier direct replay; drop 429 rotation

@luqman-v1 luqman-v1 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM: replays OpenCode 403 FreeTierError once on direct egress when the proxy IP is flagged, while properly respecting strictProxy configurations. Safe and well tested.

rqzbeh added a commit to rqzbeh/KhatamRouter that referenced this pull request Oct 25, 2026
…th security hardening (#67)

* fix(translator): downgrade deepseek json_schema to json_object

Ports decolua/9router#5150

DeepSeek documents only json_object; json_schema returns HTTP 400
"This response_format type is unavailable now". Downgrade json_schema
to json_object so structured requests continue in JSON mode rather than
hard-failing. Optional chaining guarantees non-object response_format
values cannot throw.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix(capabilities): cap oc/deepseek-v4.1-flash-free context window

Ports decolua/9router#5179

The OpenCode free backend rejects requests above ~330k prompt tokens
(upstream issue #5097). While upstream previously matched a generic 1M
pattern and capped it to 330000, KhatamRouter already safe-listed this
model at 200000/32000 based on models.dev. Explicitly pin the entry in
PROVIDER_CAPABILITIES.opencode at 200000/32000 to ensure contextWindow
<= 330000 and maxOutput <= contextWindow, preserving vision and reasoning
without raising the advertised token window beyond our safe baseline.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix(providers): avoid network probe on opencode free tier connection test

Ports decolua/9router#5193

The OpenCode free lane has no secret to validate (it uses "Bearer public"),
and the previous probe sent an outdated "opencode/1.18.x" user-agent fingerprint
to https://opencode.ai/zen/v1/models which rejects with 403 FreeTierError.
Return valid: true directly to prevent spurious failures and unnecessary network
egress, while keeping the opencode-go authenticated probe untouched.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* feat(opencode): port session timestamps and CLI fingerprint (#5222)

Ports decolua/9router#5222:
- Mint translated OpenCode session IDs with Date.now() and memoize per
  (clientTool, sessionId) with bounded Map and MEMORY_CONFIG TTL cleanup.
- Bump default OpenCode User-Agent to opencode/latest/2.1.4/cli.
- Accept shell-description platform wordings (linux/darwin/win32) as valid
  fingerprint definitions.
- Apply <env> environment marker (applyEnvFingerprint) across Responses,
  Chat Completions, and Claude body shapes (with fail-open and idempotency).
- Support Claude-shaped body fingerprint placement for messages models
  (union-alpha) via top-level system field.

Security deviation from upstream:
Upstream dynamically reads process.cwd() and os.tmpdir() to construct the
<env> block, disclosing the gateway host's filesystem layout, username,
and directory structure to external upstreams on every request. KhatamRouter
substitutes fixed neutral constants (/home/user/project, /var/cache/opencode,
"linux") that satisfy the upstream gate while preventing host reconnaissance.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* feat(proxy): replay proxied 403 FreeTierError once on direct egress

Ports decolua/9router#4988. When an OpenCode free-tier request receives a 403
FreeTierError because the proxy's egress IP has been flagged by the gate,
replay the request once on direct egress.

Hardened with the following security deviations from upstream:
- Restrict replay exclusively to OpenCode host (opencode.ai and subdomains).
- Honour strictProxy (and direct fallback prevention) without exception.
- Cap buffered body inspection at 64 KiB to prevent DoS via unbounded buffering.
- Replay at most once with identical method/body/headers, logging only the host.
- Reject replay for consumed/locked stream request bodies and return rebuilt response.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* docs(upstream): record pre-Wave-8 Decolua 9router ports and audit ledger

Ports decolua/9router#4988, decolua/9router#5222, decolua/9router#5150,
decolua/9router#5179, and decolua/9router#5193:
- Document what was ported across all 5 PRs into docs/UPSTREAM-PORTS.md
- Record all security deviations and hardening invariants in docs/UPSTREAM-PORTS.md
- Add pre-Wave-8 upstream port section to CHANGELOG.md

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.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.

3 participants