mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
This commit is contained in:
committed by
GitHub
parent
3e79d92744
commit
c9e24ae48c
@@ -9,6 +9,7 @@
|
||||
### 🐛 Fixed
|
||||
|
||||
- **fix(intelligence): run pricing + models.dev sync from the live startup path** — like the Arena ELO sync (v3.8.24), the external **pricing sync** (`PRICING_SYNC_ENABLED`) and the **models.dev capability sync** (Settings → AI toggle) were only initialized from `server-init.ts`, which the Next standalone runtime never executes — and models.dev had no caller at all. Their toggles were inert in production. Both are now initialized from `instrumentation-node.ts` (self-gated, opt-in preserved, non-blocking, never fatal). (thanks @diegosouzapw)
|
||||
- **fix(grok-web): clearer 403 message for anti-bot / IP-reputation blocks** — a Grok Web subscription validating from a flagged datacenter/VPS IP got a 403 that read like an invalid cookie, sending users to chase a cookie that was actually fine. A non-auth 403 (Cloudflare challenge / anti-bot body) now returns a message stating the cookie is likely OK and the block is IP-reputation-based — retry from a residential IP or configure a proxy (auth-shaped 403s keep the re-paste guidance). ([#3474](https://github.com/diegosouzapw/OmniRoute/issues/3474) — thanks @friedtofu1608)
|
||||
- **fix(db): make the mass-pending-migrations safety threshold env-overridable** — restoring a backup DB from an older version could trip "Detected N pending migrations … threshold is 50" with no way to override the hardcoded `50`. The threshold is now configurable via `OMNIROUTE_MAX_PENDING_MIGRATIONS` (resolved at startup; `0` disables the check). ([#3416](https://github.com/diegosouzapw/OmniRoute/issues/3416) — thanks @samuraiIT)
|
||||
- **test(proxy): cover the Vercel-relay `proxyFetch` path** — net-new tests for `buildVercelRelayHeaders` and the `vercel`-type relay short-circuit (`x-relay-target`/`-path`/`-auth`, TCP-skip, missing-auth fail-closed), closing one of the deferred test gaps tracked in [#2743](https://github.com/diegosouzapw/OmniRoute/issues/2743). (thanks @diegosouzapw)
|
||||
- **fix(cli): surface `omniroute runtime repair` in the native-module error messages** — after a Node major upgrade, `better-sqlite3`'s prebuilt binary mismatches the ABI and the service can crash-loop; the error only mentioned `npm rebuild better-sqlite3` (which fails for global / no-toolchain installs). The startup + SQLite error hints now also point to the existing self-heal command `omniroute runtime repair` (rebuilds into a user-writable runtime), and a top-level `omniroute repair` alias was added. ([#3476](https://github.com/diegosouzapw/OmniRoute/issues/3476) — thanks @Rahulsharma0810)
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
"src/lib/evals/evalRunner.ts": 961,
|
||||
"src/lib/memory/retrieval.ts": 1171,
|
||||
"src/lib/modelsDevSync.ts": 934,
|
||||
"src/lib/providers/validation.ts": 4302,
|
||||
"src/lib/providers/validation.ts": 4348,
|
||||
"src/lib/tailscaleTunnel.ts": 1189,
|
||||
"src/lib/usage/callLogs.ts": 975,
|
||||
"src/lib/usage/providerLimits.ts": 949,
|
||||
@@ -116,5 +116,6 @@
|
||||
"_rebaseline_2026_06_13_3786_agy_fallback": "Re-baseline #3786 (agy Pro-family upstream-id fallback chain): antigravity.ts 1572→1649 (+77). Crescimento = split de execute() em driver + executeOnce(modelIdOverride) para retentar ids alternativos no 400 (gemini-3.1-pro-high→gemini-pro-agent→gemini-3-pro-high), threading do override em transformRequest/cleanModelName. Lógica coesa de retry no executor — não é bloco movível (chama this.executeOnce). A parte pura (ANTIGRAVITY_PRO_FALLBACK_CHAINS + getAntigravityModelFallbacks) ficou em antigravityModelAliases.ts. Os 3 drifts release-wide (ProxyRegistryManager/sidebarVisibility/schemas) são do #3809 do owner, não deste PR.",
|
||||
"_rebaseline_2026_06_13_3782_hide_persist": "Re-baseline #3782 (preservar modelos eye-hidden no auto-sync): models.ts 1132→1180 (+48). Crescimento = flag distinto isDeleted em ModelCompatOverride/ModelCompatPatch + handling em mergeModelCompatOverride + helper getModelIsDeleted, para separar 'deletado' (trash, dropado no re-sync #3199) de 'oculto' (eye toggle, preservado). Lógica coesa de visibilidade no módulo db; não-extraível. Os 3 drifts release-wide (ProxyRegistryManager/sidebarVisibility/schemas) são do #3809 do owner, não deste PR.",
|
||||
"_rebaseline_2026_06_13_3758_chat_early_eof": "Re-baseline #3758 (#3817 mergeado): chat.ts 1392→1425 (+33). Crescimento = retry bounded de STREAM_EARLY_EOF no handleSingleModelChat (contador streamEarlyEofRetries + bloco de retry guardado por shouldRetryStreamEarlyEof). Lógica coesa no handler de chat; não-extraível. Reconciliação tardia — o bump foi esquecido no PR do fix (o de antigravity/models foi feito).",
|
||||
"_rebaseline_2026_06_13_3416_migration_threshold": "Re-baseline #3416 (threshold de migrações pendentes via env): migrationRunner.ts 1100→1125 (+25). Crescimento = helper resolveMaxPendingMigrations() que lê OMNIROUTE_MAX_PENDING_MIGRATIONS em call-time (valida finito+>=0, fallback 50) + JSDoc. Lógica coesa de config no runner; não-extraível."
|
||||
"_rebaseline_2026_06_13_3416_migration_threshold": "Re-baseline #3416 (threshold de migrações pendentes via env): migrationRunner.ts 1100→1125 (+25). Crescimento = helper resolveMaxPendingMigrations() que lê OMNIROUTE_MAX_PENDING_MIGRATIONS em call-time (valida finito+>=0, fallback 50) + JSDoc. Lógica coesa de config no runner; não-extraível.",
|
||||
"_rebaseline_2026_06_13_3474_grok_403": "Re-baseline #3474 (mensagem clara no 403 anti-bot do Grok): validation.ts 4302→4348 (+46). Crescimento = helper isGrokAntiBotBlock() + branch 403 de 3 tiers (auth-shaped / anti-bot-IP-reputation / upstream-error). Lógica coesa de classificação no validator; não-extraível."
|
||||
}
|
||||
|
||||
@@ -2905,6 +2905,35 @@ async function validateQwenWebProvider({ apiKey }: any) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristic for a Grok 403 that is an anti-bot / IP-reputation block rather than
|
||||
* a genuine upstream API error (issue #3474).
|
||||
*
|
||||
* Returns true when the body reads like an anti-bot rejection — Grok's literal
|
||||
* "Request rejected by anti-bot rules." text, or a bare/non-structured forbidden
|
||||
* body that carries no parseable upstream `error.message`. Returns false for a
|
||||
* structured upstream API error (e.g. `{"error":{"message":"Model is not found"}}`),
|
||||
* which must keep surfacing its body to the user/maintainer.
|
||||
*
|
||||
* Callers should run `isCloudflareChallenge()` first; this covers the non-HTML
|
||||
* anti-bot cases that Cloudflare-challenge detection does not.
|
||||
*/
|
||||
function isGrokAntiBotBlock(body: string | null | undefined): boolean {
|
||||
const text = (body || "").trim();
|
||||
if (!text) return true; // empty 403 body — pre-auth block, treat as anti-bot
|
||||
if (/anti-bot|forbidden|access denied|blocked|rate.?limit/i.test(text)) return true;
|
||||
// A structured upstream API error has a parseable JSON `error.message`; if one
|
||||
// is present this is a real upstream error, not an anti-bot block.
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
if (parsed && typeof parsed?.error?.message === "string") return false;
|
||||
} catch {
|
||||
// Non-JSON 403 body with no recognizable structure → treat as anti-bot block.
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function validateGrokWebProvider({ apiKey, providerSpecificData = {} }: any) {
|
||||
try {
|
||||
const token = extractCookieValue(apiKey, "sso");
|
||||
@@ -3022,17 +3051,34 @@ async function validateGrokWebProvider({ apiKey, providerSpecificData = {} }: an
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
// Grok uses 403 for auth failures, entitlement issues, geo blocks, and
|
||||
// resource errors. Default-deny: only the auth-shaped 403 gets the
|
||||
// re-paste hint; anything else surfaces the upstream body so the user
|
||||
// (or maintainer, if upstream renames the probe model) sees the real
|
||||
// cause instead of a misleading "valid" verdict.
|
||||
// Grok uses 403 for auth failures, entitlement issues, geo blocks,
|
||||
// anti-bot/IP-reputation rejections, and resource errors. Classify before
|
||||
// messaging — a misleading "invalid cookie" verdict on an IP-reputation
|
||||
// block (issue #3474) sends users chasing a cookie that is actually fine.
|
||||
//
|
||||
// 1. Auth-shaped → the cookie/session is the problem; re-paste it.
|
||||
if (/invalid-credentials|unauthenticated|unauthorized/i.test(errorDetail)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: "Invalid SSO cookie — re-paste from grok.com DevTools → Cookies → sso",
|
||||
};
|
||||
}
|
||||
// 2. Anti-bot / Cloudflare / IP-reputation block → the cookie is likely
|
||||
// fine; the request was rejected before auth was even evaluated. This is
|
||||
// not code-fixable: the datacenter/VPS IP is flagged. A Cloudflare
|
||||
// challenge body, Grok's "anti-bot rules" rejection, or a bare/non-JSON
|
||||
// forbidden body (no structured upstream `error.message`) all map here.
|
||||
if (isCloudflareChallenge(errorDetail) || isGrokAntiBotBlock(errorDetail)) {
|
||||
return {
|
||||
valid: false,
|
||||
error:
|
||||
"Grok returned 403 (anti-bot/Cloudflare block). Your sso cookie is likely fine — " +
|
||||
"this is an IP-reputation block on the request, not an auth failure. Retry from a " +
|
||||
"residential IP or configure a proxy for grok-web.",
|
||||
};
|
||||
}
|
||||
// 3. Structured upstream error (e.g. probe model renamed) → surface the body
|
||||
// so the user/maintainer sees the real cause instead of a wrong verdict.
|
||||
return {
|
||||
valid: false,
|
||||
error: `Grok rejected validation (403)${errorDetail ? `: ${errorDetail.slice(0, 160)}` : ""}`,
|
||||
|
||||
@@ -478,14 +478,99 @@ test("grok-web validator: non-auth 403 is reported as failure with upstream body
|
||||
assert.match(result.error || "", /Model is not found/);
|
||||
});
|
||||
|
||||
test("grok-web validator: generic 403 forbidden is rejected, not silently passed", async () => {
|
||||
test("grok-web validator: generic non-auth 403 maps to IP-reputation guidance, not 'invalid cookie' (#3474)", async () => {
|
||||
__setGrokTlsFetchOverride(async () => {
|
||||
return { status: 403, headers: new Headers(), text: "Forbidden", body: null };
|
||||
});
|
||||
|
||||
const result = await validateProviderApiKey({ provider: "grok-web", apiKey: "any-cookie" });
|
||||
assert.equal(result.valid, false);
|
||||
// A bare/non-auth 403 from grok.com is almost always an anti-bot/IP-reputation
|
||||
// block — the cookie itself is likely fine. The message must point the user to a
|
||||
// residential IP or proxy, NOT tell them the cookie is invalid.
|
||||
assert.match(result.error || "", /residential IP|proxy/i);
|
||||
assert.doesNotMatch(result.error || "", /invalid SSO cookie/i);
|
||||
});
|
||||
|
||||
test("grok-web validator: anti-bot 'Request rejected' 403 maps to IP-reputation guidance (#3474)", async () => {
|
||||
__setGrokTlsFetchOverride(async () => {
|
||||
return {
|
||||
status: 403,
|
||||
headers: new Headers(),
|
||||
text: "Request rejected by anti-bot rules.",
|
||||
body: null,
|
||||
};
|
||||
});
|
||||
|
||||
const result = await validateProviderApiKey({ provider: "grok-web", apiKey: "good-cookie" });
|
||||
assert.equal(result.valid, false);
|
||||
assert.match(result.error || "", /residential IP|proxy/i);
|
||||
// Cookie may be fine — must not claim it is invalid/expired.
|
||||
assert.doesNotMatch(result.error || "", /invalid SSO cookie|expired/i);
|
||||
});
|
||||
|
||||
test("grok-web validator: Cloudflare challenge returned with a 403 status maps to IP guidance (#3474)", async () => {
|
||||
__setGrokTlsFetchOverride(async () => {
|
||||
return {
|
||||
status: 403,
|
||||
headers: new Headers(),
|
||||
text: "<html><title>Just a moment...</title><script>window._cf_chl_opt</script></html>",
|
||||
body: null,
|
||||
};
|
||||
});
|
||||
|
||||
const result = await validateProviderApiKey({ provider: "grok-web", apiKey: "sso=abc" });
|
||||
assert.equal(result.valid, false);
|
||||
assert.match(result.error || "", /residential IP|proxy/i);
|
||||
});
|
||||
|
||||
test("grok-web validator: IP-reputation 403 message does not leak a stack/raw error (Hard Rule #12) (#3474)", async () => {
|
||||
__setGrokTlsFetchOverride(async () => {
|
||||
return {
|
||||
status: 403,
|
||||
headers: new Headers(),
|
||||
text: "Request rejected by anti-bot rules.\n at GrokWeb.validate (/app/secret/path.ts:42:13)",
|
||||
body: null,
|
||||
};
|
||||
});
|
||||
|
||||
const result = await validateProviderApiKey({ provider: "grok-web", apiKey: "good-cookie" });
|
||||
assert.equal(result.valid, false);
|
||||
assert.match(result.error || "", /residential IP|proxy/i);
|
||||
assert.ok(!(result.error || "").includes("at GrokWeb.validate"));
|
||||
assert.ok(!(result.error || "").includes("/app/secret/path.ts"));
|
||||
assert.ok(!(result.error || "").includes(" at "));
|
||||
});
|
||||
|
||||
test("grok-web validator: structured non-auth 403 (resource error) still surfaces upstream body for maintainers (#3474)", async () => {
|
||||
// Distinct from an anti-bot block: a genuine upstream API error (e.g. the probe
|
||||
// model was renamed) carries a structured error.message and must NOT be masked
|
||||
// by the IP-reputation guidance — the maintainer needs to see the real cause.
|
||||
__setGrokTlsFetchOverride(async () => {
|
||||
return {
|
||||
status: 403,
|
||||
headers: new Headers(),
|
||||
text: JSON.stringify({ error: { code: 7, message: "Model is not found", details: [] } }),
|
||||
body: null,
|
||||
};
|
||||
});
|
||||
|
||||
const result = await validateProviderApiKey({ provider: "grok-web", apiKey: "good-cookie" });
|
||||
assert.equal(result.valid, false);
|
||||
assert.match(result.error || "", /Grok rejected validation \(403\)/);
|
||||
assert.match(result.error || "", /Model is not found/);
|
||||
assert.doesNotMatch(result.error || "", /residential IP|proxy/i);
|
||||
});
|
||||
|
||||
test("grok-web validator: auth-shaped 401 keeps the re-paste/re-authenticate guidance (no regression) (#3474)", async () => {
|
||||
__setGrokTlsFetchOverride(async () => {
|
||||
return { status: 401, headers: new Headers(), text: "Unauthorized", body: null };
|
||||
});
|
||||
|
||||
const result = await validateProviderApiKey({ provider: "grok-web", apiKey: "expired-cookie" });
|
||||
assert.equal(result.valid, false);
|
||||
assert.match(result.error || "", /Invalid SSO cookie|re-paste/i);
|
||||
assert.doesNotMatch(result.error || "", /residential IP|proxy/i);
|
||||
});
|
||||
|
||||
test("grok-web validator: 403 with credential-rejection body is treated as auth-failed", async () => {
|
||||
|
||||
Reference in New Issue
Block a user