/** * 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 = /(? 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}`; }