fix(providers): treat recoverable Antigravity/Cloud-Code 403s as project errors, not account bans (#6452)

fix(providers): treat recoverable Antigravity/CF 403 as retryable (net +1/-0, test OK). Integrated into release/v3.8.46.
This commit is contained in:
Jillur Rahman
2026-07-07 06:16:39 +06:00
committed by GitHub
parent 62a74faa4b
commit 27867e5af3
3 changed files with 74 additions and 1 deletions

View File

@@ -31,6 +31,7 @@
### 🐛 Bug Fixes
- **fix(providers):** recoverable Antigravity / Cloudflare `403` responses are now classified as retryable instead of terminal, so a transient WAF block no longer bans the connection. Regression guard: `tests/unit/errorclassifier-antigravity-403.test.ts`. (thanks @developerjillur)
- **fix(mitm):** `sanitizeHeaders` now redacts `Set-Cookie` response headers so upstream session cookies never leak into logs / diagnostics. Regression guard: `tests/unit/mitm-sanitize-headers.test.ts`. (thanks @developerjillur)
- **fix(api):** `/api/compression/preview` now accepts `mode: "caveman"` and correctly handles stacked / zero-compression previews ([#6425](https://github.com/diegosouzapw/OmniRoute/issues/6425)). Regression guard: `tests/unit/api/compression-preview-caveman-and-stacked-6425.test.ts`. (thanks @chirag127)
- **feat(providers):** add **Zed** hosted LLM aggregator as a native-app provider ([#6118](https://github.com/diegosouzapw/OmniRoute/pull/6118)) — OAuth sign-in via the Zed hosted flow, registered through the shared provider registry + executor. Regression guards: `tests/unit/zed-oauth-provider.test.ts`, `zed-import-utils.test.ts`, `zed-docker-detect.test.ts`, `mitm-handler-zed.test.ts`. VPS-validated via live operator login (Hard Rule #18).

View File

@@ -158,7 +158,29 @@ export function classifyProviderError(
return PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED;
}
if (statusCode === 403) {
if (bodyStr.includes("has not been used in project")) {
// Cloud Code / Antigravity (Gemini Code Assist) 403s are almost always a
// RECOVERABLE project-config issue — the Cloud AI Companion API not enabled
// on the project ("has not been used in project …", SERVICE_DISABLED,
// accessNotConfigured), a stale/mismatched project, or PERMISSION_DENIED on
// the project — NOT an account ban. Real account bans are already caught by
// isAccountDeactivated above (→ ACCOUNT_DEACTIVATED). Classifying these as
// PROJECT_ROUTE_ERROR keeps the account active and recoverable once the
// project/API is fixed, instead of permanently disabling it on a single
// fixable 403 (which previously required a full OAuth reconnect). (antigravity-403)
const p = (provider || "").toLowerCase();
const isCloudCodeProvider =
p === "antigravity" ||
p === "gemini-cli" ||
p.includes("cloudcode") ||
p.includes("cloud-code");
const recoverableProject403 =
bodyStr.includes("has not been used in project") ||
bodyStr.includes("SERVICE_DISABLED") ||
bodyStr.includes("accessNotConfigured") ||
bodyStr.includes("PERMISSION_DENIED") ||
/\bit is disabled\b/i.test(bodyStr) ||
isCloudCodeProvider;
if (recoverableProject403) {
return PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR;
}
if (provider && getProviderCategory(provider) === "apikey") {

View File

@@ -0,0 +1,50 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
classifyProviderError,
PROVIDER_ERROR_TYPES,
} from "../../open-sse/services/errorClassifier.ts";
// A Cloud Code / Antigravity (Gemini Code Assist) 403 is almost always a
// RECOVERABLE project-config problem — the Cloud AI Companion API not enabled on
// the project, a stale project, or PERMISSION_DENIED — NOT an account ban.
// It must classify as PROJECT_ROUTE_ERROR so the account stays usable once the
// project is fixed, instead of being disabled for ~a year like a real ban.
test("403 'has not been used in project' (antigravity) -> PROJECT_ROUTE_ERROR", () => {
const body = {
error: {
code: 403,
status: "PERMISSION_DENIED",
message:
"Cloud AI Companion API has not been used in project 123 before or it is disabled.",
},
};
assert.equal(
classifyProviderError(403, body, "antigravity"),
PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR,
);
});
test("403 SERVICE_DISABLED / PERMISSION_DENIED (gemini-cli) -> PROJECT_ROUTE_ERROR", () => {
const body = { error: { status: "PERMISSION_DENIED", details: [{ reason: "SERVICE_DISABLED" }] } };
assert.equal(
classifyProviderError(403, body, "gemini-cli"),
PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR,
);
});
test("403 on a cloud-code provider with a bare body -> still recoverable PROJECT_ROUTE_ERROR", () => {
assert.equal(
classifyProviderError(403, "forbidden", "antigravity-cloudcode"),
PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR,
);
});
test("403 real ban signal -> still ACCOUNT_DEACTIVATED (ban detection preserved)", () => {
const body = "This service has been disabled in this account for violation of policy.";
assert.equal(
classifyProviderError(403, body, "antigravity"),
PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED,
);
});