From 0aebdfd6fd3db5de494c172890e7d2a122649fdd Mon Sep 17 00:00:00 2001 From: agentHits Date: Fri, 4 Sep 2026 16:06:54 +0300 Subject: [PATCH 1/2] fix(google): classify location not supported as permission error instead of invalid_request --- src/adapters/google-errors.ts | 19 +++++++++++++++++ src/adapters/google-http.ts | 18 ++++++++++++++-- src/lib/errors.ts | 20 ++++++++++++++++++ tests/adapters/google/google-errors.test.ts | 23 +++++++++++++++++++++ tests/server/error-fidelity.test.ts | 12 +++++++++++ 5 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/adapters/google-errors.ts b/src/adapters/google-errors.ts index d78ee1fb9b..0dbcfce86e 100644 --- a/src/adapters/google-errors.ts +++ b/src/adapters/google-errors.ts @@ -54,11 +54,30 @@ export function isGoogleQuotaExhaustedText(text: string): boolean { return GOOGLE_QUOTA_EXHAUSTED_NEEDLES.some(needle => lower.includes(needle)); } +export const GOOGLE_LOCATION_UNSUPPORTED_PATTERNS = [ + "location is not supported", + "location not supported", + "unsupported location", + "region is not supported", + "unsupported region", + "country is not supported", + "not supported in your country", + "not supported in your region", + "not supported for the api use", +]; + +export function isGoogleLocationUnsupportedText(text: string): boolean { + const lower = text.toLowerCase(); + return GOOGLE_LOCATION_UNSUPPORTED_PATTERNS.some(needle => lower.includes(needle)); +} function classifyGoogle(label: string, status: number | undefined, enumStatus: string | undefined, text: string): string { const lower = `${enumStatus ?? ""} ${text}`.toLowerCase(); const quotaExhausted = isGoogleQuotaExhaustedText(lower); if ((!enumStatus || enumStatus === "RESOURCE_EXHAUSTED") && quotaExhausted) return `${label} quota exhausted`; + if (isGoogleLocationUnsupportedText(lower)) { + return `${label} location not supported`; + } if (status === 429 || enumStatus === "RESOURCE_EXHAUSTED" || lower.includes("rate limit")) { return `${label} rate limit exceeded`; } diff --git a/src/adapters/google-http.ts b/src/adapters/google-http.ts index f7b90de87e..c14657b8d7 100644 --- a/src/adapters/google-http.ts +++ b/src/adapters/google-http.ts @@ -1,5 +1,10 @@ import type { AdapterFetchContext, AdapterRequest } from "./base"; -import { isQuotaExhaustedBody, retryableGoogleStatus, safeGoogleHttpErrorMessage } from "./google-errors"; +import { + isGoogleLocationUnsupportedText, + isQuotaExhaustedBody, + retryableGoogleStatus, + safeGoogleHttpErrorMessage, +} from "./google-errors"; import { repairGoogleInvalidRequestBody } from "./google-wire-compiler"; import { normalizeUpstreamHttpErrorResponse, readDisplaySafeErrorPayloadText } from "./upstream-http-error"; import { @@ -22,7 +27,16 @@ export interface GoogleRetryOptions { async function normalizeFinalGoogleError(label: string, res: Response, signal?: AbortSignal): Promise { return normalizeUpstreamHttpErrorResponse(res, { signal, - formatMessage: payloadText => safeGoogleHttpErrorMessage(label, res.status, payloadText), + formatMessage: payloadText => { + const message = safeGoogleHttpErrorMessage(label, res.status, payloadText); + if (isGoogleLocationUnsupportedText(payloadText)) { + console.warn( + `[opencodex] ${label} request was rejected because the client location is not supported. ` + + "If using a proxy or VPN, verify TUN mode is enabled and check for IPv6 / direct routing leaks.", + ); + } + return message; + }, }); } diff --git a/src/lib/errors.ts b/src/lib/errors.ts index 384f7c2d44..68fed21dab 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -127,6 +127,20 @@ function isPermissionMessage(text: string): boolean { ); } +export function isLocationUnsupportedMessage(text: string): boolean { + return ( + text.includes("location is not supported") || + text.includes("location not supported") || + text.includes("unsupported location") || + text.includes("region is not supported") || + text.includes("unsupported region") || + text.includes("country is not supported") || + text.includes("not supported in your country") || + text.includes("not supported in your region") || + text.includes("not supported for the api use") + ); +} + /** * Client cancelled / closed the turn. Matches ONLY abort phrases this codebase * produces — "client closed request during web-search" (src/web-search/loop.ts), @@ -251,6 +265,12 @@ export function classifyError(status: number, type: string, message: string): Oc ) { return { message, type: "permission_error", code: "subscription_required" }; } + if ( + type === "location_not_supported" || + isLocationUnsupportedMessage(text) + ) { + return { message, type: "permission_error", code: "location_not_supported" }; + } if ( status === 403 || type === "permission_error" || diff --git a/tests/adapters/google/google-errors.test.ts b/tests/adapters/google/google-errors.test.ts index c31da10556..64e4c7745f 100644 --- a/tests/adapters/google/google-errors.test.ts +++ b/tests/adapters/google/google-errors.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; import { + isGoogleLocationUnsupportedText, isGoogleQuotaExhaustedText, isQuotaExhaustedBody, safeAntigravityHttpErrorMessage, @@ -86,4 +87,26 @@ describe("google error classification & quota exhaustion", () => { }); expect(isQuotaExhaustedBody(jsonBody)).toBe(false); }); + + test("classifies location unsupported as location not supported instead of invalid request", () => { + const locationPhrases = [ + "User location is not supported for the API use.", + "Location not supported in your region.", + "This model is not supported in your country.", + "Service is not supported for the api use in this location.", + ]; + + for (const phrase of locationPhrases) { + expect(isGoogleLocationUnsupportedText(phrase)).toBe(true); + const jsonBody = JSON.stringify({ + error: { + code: 400, + status: "FAILED_PRECONDITION", + message: phrase, + }, + }); + expect(safeAntigravityHttpErrorMessage(400, jsonBody)).toContain("Antigravity location not supported"); + expect(safeVertexHttpErrorMessage(400, jsonBody)).toContain("Vertex AI location not supported"); + } + }); }); diff --git a/tests/server/error-fidelity.test.ts b/tests/server/error-fidelity.test.ts index c3044aaa4e..30c3fdaea2 100644 --- a/tests/server/error-fidelity.test.ts +++ b/tests/server/error-fidelity.test.ts @@ -91,6 +91,18 @@ describe("error fidelity", () => { type: "insufficient_quota", code: "insufficient_quota", }); + expect(classifyError(400, "upstream_error", "Antigravity location not supported: User location is not supported for the API use.")).toMatchObject({ + type: "permission_error", + code: "location_not_supported", + }); + expect(classifyError(400, "upstream_error", "User location is not supported for the API use.")).toMatchObject({ + type: "permission_error", + code: "location_not_supported", + }); + expect(classifyError(403, "location_not_supported", "Region is not supported")).toMatchObject({ + type: "permission_error", + code: "location_not_supported", + }); }); test("formatErrorResponse returns OpenAI-compatible classified error envelope", async () => { From 0226f612f3103907d5ca01d680fb5f3cf7a7195a Mon Sep 17 00:00:00 2001 From: agentHits Date: Fri, 4 Sep 2026 17:23:37 +0300 Subject: [PATCH 2/2] refactor(google): constrain location pattern, deduplicate needles and add diagnostic warn tests --- src/adapters/google-errors.ts | 19 ++----------- src/lib/errors.ts | 24 ++++++++-------- tests/adapters/google/google-errors.test.ts | 2 +- .../google/google-vertex-http.test.ts | 28 +++++++++++++++++++ tests/server/error-fidelity.test.ts | 9 +++++- 5 files changed, 53 insertions(+), 29 deletions(-) diff --git a/src/adapters/google-errors.ts b/src/adapters/google-errors.ts index 0dbcfce86e..97c63ec3a3 100644 --- a/src/adapters/google-errors.ts +++ b/src/adapters/google-errors.ts @@ -1,4 +1,5 @@ import { parseUpstreamJsonPayload, safeUpstreamErrorString, sanitizeUpstreamErrorText } from "./upstream-http-error"; +import { isLocationUnsupportedMessage, LOCATION_UNSUPPORTED_PATTERNS } from "../lib/errors"; /** Pull the human detail out of the Google API error envelope `{error:{message,status,code}}`. */ function googleErrorDetail(payloadText: string): { message?: string; status?: string } { @@ -54,22 +55,8 @@ export function isGoogleQuotaExhaustedText(text: string): boolean { return GOOGLE_QUOTA_EXHAUSTED_NEEDLES.some(needle => lower.includes(needle)); } -export const GOOGLE_LOCATION_UNSUPPORTED_PATTERNS = [ - "location is not supported", - "location not supported", - "unsupported location", - "region is not supported", - "unsupported region", - "country is not supported", - "not supported in your country", - "not supported in your region", - "not supported for the api use", -]; - -export function isGoogleLocationUnsupportedText(text: string): boolean { - const lower = text.toLowerCase(); - return GOOGLE_LOCATION_UNSUPPORTED_PATTERNS.some(needle => lower.includes(needle)); -} +export const GOOGLE_LOCATION_UNSUPPORTED_PATTERNS = LOCATION_UNSUPPORTED_PATTERNS; +export const isGoogleLocationUnsupportedText = isLocationUnsupportedMessage; function classifyGoogle(label: string, status: number | undefined, enumStatus: string | undefined, text: string): string { const lower = `${enumStatus ?? ""} ${text}`.toLowerCase(); diff --git a/src/lib/errors.ts b/src/lib/errors.ts index 68fed21dab..e35e35ebd1 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -127,18 +127,20 @@ function isPermissionMessage(text: string): boolean { ); } +export const LOCATION_UNSUPPORTED_PATTERNS = [ + "location is not supported", + "location not supported", + "unsupported location", + "region is not supported", + "unsupported region", + "country is not supported", + "not supported in your country", + "not supported in your region", +] as const; + export function isLocationUnsupportedMessage(text: string): boolean { - return ( - text.includes("location is not supported") || - text.includes("location not supported") || - text.includes("unsupported location") || - text.includes("region is not supported") || - text.includes("unsupported region") || - text.includes("country is not supported") || - text.includes("not supported in your country") || - text.includes("not supported in your region") || - text.includes("not supported for the api use") - ); + const lower = text.toLowerCase(); + return LOCATION_UNSUPPORTED_PATTERNS.some(needle => lower.includes(needle)); } /** diff --git a/tests/adapters/google/google-errors.test.ts b/tests/adapters/google/google-errors.test.ts index 64e4c7745f..d2b86297f3 100644 --- a/tests/adapters/google/google-errors.test.ts +++ b/tests/adapters/google/google-errors.test.ts @@ -93,7 +93,7 @@ describe("google error classification & quota exhaustion", () => { "User location is not supported for the API use.", "Location not supported in your region.", "This model is not supported in your country.", - "Service is not supported for the api use in this location.", + "Unsupported location for the API use.", ]; for (const phrase of locationPhrases) { diff --git a/tests/adapters/google/google-vertex-http.test.ts b/tests/adapters/google/google-vertex-http.test.ts index 8ad138741d..bf0c99120b 100644 --- a/tests/adapters/google/google-vertex-http.test.ts +++ b/tests/adapters/google/google-vertex-http.test.ts @@ -192,6 +192,34 @@ describe("vertex retry fetch", () => { expect(text).not.toContain("secret-token"); }); + test("Antigravity location unsupported logs diagnostic warning and classifies correctly", async () => { + const warnings: string[] = []; + const origWarn = console.warn; + console.warn = (...args: unknown[]) => { warnings.push(args.map(String).join(" ")); }; + try { + mockFetch([new Response(vertexError(400, "FAILED_PRECONDITION", "User location is not supported for the API use."), { status: 400 })]); + const res = await fetchAntigravityWithRetry(request, { timeoutMs: 5_000 }); + const text = await res.text(); + expect(text).toContain("Antigravity location not supported"); + expect(warnings.some(w => w.includes("client location is not supported") && w.includes("TUN mode"))).toBe(true); + } finally { + console.warn = origWarn; + } + }); + + test("Antigravity non-location error does not log location diagnostic warning", async () => { + const warnings: string[] = []; + const origWarn = console.warn; + console.warn = (...args: unknown[]) => { warnings.push(args.map(String).join(" ")); }; + try { + mockFetch([new Response(vertexError(400, "INVALID_ARGUMENT", "syntax error"), { status: 400 })]); + await fetchAntigravityWithRetry(request, { timeoutMs: 5_000 }); + expect(warnings.some(w => w.includes("client location is not supported"))).toBe(false); + } finally { + console.warn = origWarn; + } + }); + test("does not retry 401/403 (single attempt)", async () => { const mock401 = mockFetch([new Response(vertexError(401, "UNAUTHENTICATED", "bad token"), { status: 401 })]); const res401 = await fetchVertexWithRetry(request, { timeoutMs: 5_000 }); diff --git a/tests/server/error-fidelity.test.ts b/tests/server/error-fidelity.test.ts index 30c3fdaea2..0c3f813661 100644 --- a/tests/server/error-fidelity.test.ts +++ b/tests/server/error-fidelity.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { bridgeToResponsesSSE, formatErrorResponse } from "../../src/bridge"; -import { classifyError } from "../../src/lib/errors"; +import { classifyError, isLocationUnsupportedMessage } from "../../src/lib/errors"; import { sanitizePassthroughHeaders } from "../../src/server"; import type { AdapterEvent } from "../../src/types"; @@ -103,6 +103,13 @@ describe("error fidelity", () => { type: "permission_error", code: "location_not_supported", }); + expect(classifyError(400, "upstream_error", "USER LOCATION IS NOT SUPPORTED IN YOUR REGION")).toMatchObject({ + type: "permission_error", + code: "location_not_supported", + }); + expect(isLocationUnsupportedMessage("USER LOCATION IS NOT SUPPORTED")).toBe(true); + expect(isLocationUnsupportedMessage("Region Is Not Supported")).toBe(true); + expect(isLocationUnsupportedMessage("not supported for the api use")).toBe(false); }); test("formatErrorResponse returns OpenAI-compatible classified error envelope", async () => {