Files
OmniRoute/open-sse/executors/opencodeGeoBlock.ts
Dizzle e325888d78 fix(sse): fail over opencode request on refused-route 403 (#13498)
Behind the new `OPENCODE_USER_BLOCKED_ROTATION` flag (default off), a 403 or 451 carrying `user_blocked` on a proxied opencode account rotates at most once to the next account instead of being returned as-is.

Maintainer rework before merge (kept the idea, no default behavior change):
- 403 and 451 are handled by one predicate (the original returned 451 without rotation), the refused account gets a cooldown and joins the tried-set, and the response body of the attempt rotated away from is cancelled.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 20:39:39 -03:00

70 lines
3.2 KiB
TypeScript

/**
* opencodeGeoBlock.ts — geo-block predicate for the opencode executor loop.
*
* Leaf module: zero internal imports (layering — errorClassifier pulls
* accountFallback + registry + DB; this file must not). The 1010 check below
* mirrors errorClassifier.isCloudflareFingerprintRejection semantics for the
* tokens this path needs; any divergence is a bug — see the parity test.
*/
// "not available in your country" is the observed opencode RegionError phrasing
// (2026-09-07 — app.log: "This model is not available in your country.");
// siblings cover the same class, not the single incident. No bare "in your
// country/region": location text without the full prefix is not a geo signal.
// `user_blocked` refusal (observed 2026-09-13 — upstream 403 with this token).
// Rotation on it is opt-in (OPENCODE_USER_BLOCKED_ROTATION) and bounded to one
// hop; when enabled it reuses the geo tried-set. 403 and 451 are classified the
// same way. Literal exact token only; `user-blocked` / `user blocked` are
// unobserved phrasings (fail closed).
const USER_BLOCKED_SIGNAL = "user_blocked";
const GEO_SIGNALS = [
"not available in your country",
"not available in your region",
"unsupported_country",
"unsupported country",
];
// `regionerror` word-bounded: bare substring would match region_error /
// region-error variants, which are unobserved phrasings (fail closed).
const REGION_ERROR_REGEX = /(?<![A-Za-z0-9_-])regionerror(?![A-Za-z0-9_-])/i;
// Fingerprint-first: a CDN 1010 rejection says nothing about account health —
// it must never rotate as geo. Parity with errorClassifier
// isCloudflareFingerprintRejection: the bare number 1010 alone is NOT a signal
// (it occurs as port/count/model token) — only with an explicit Cloudflare key
// or the unique tokens (mirrored vectors live in the parity test below).
const CLOUDFLARE_1010_KEY_REGEX =
/(?<![A-Za-z0-9_-])error[\s_-]?code[\\"':=\s]{0,12}1010(?!\w)|(?<![A-Za-z0-9_-])error[-_]\s?1010(?!\w)\/?/i;
function isFingerprintRejection(bodyText: string): boolean {
const text = String(bodyText || "");
const lower = text.toLowerCase();
return (
CLOUDFLARE_1010_KEY_REGEX.test(text) ||
lower.includes("browser_signature_banned") ||
lower.includes("fingerprint_rejection")
);
}
export function isOpencodeGeoBlocked(status: number, bodyText: string): boolean {
if (status !== 403 && status !== 451) return false;
const text = String(bodyText || "");
if (isFingerprintRejection(text)) return false;
const lower = text.toLowerCase();
if (REGION_ERROR_REGEX.test(text)) return true;
return GEO_SIGNALS.some((signal) => lower.includes(signal));
}
/** 403 or 451 whose body carries the user_blocked token and is not a geo block or 1010 rejection. */
export function isOpencodeUserBlocked(status: number, bodyText: string | null): boolean {
if (status !== 403 && status !== 451) return false;
const text = String(bodyText || "");
if (isFingerprintRejection(text) || isOpencodeGeoBlocked(status, text)) return false;
return text.toLowerCase().includes(USER_BLOCKED_SIGNAL);
}
export function proxyKeyOf(proxy: { host: string; port: number } | null): string | null {
if (!proxy) return null;
return `${proxy.host}:${proxy.port}`;
}