diff --git a/next.config.mjs b/next.config.mjs index ac7197503ce..d99ccd3ad53 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -223,6 +223,11 @@ const nextConfig = { "pino-pretty", "thread-stream", "pino-abstract-transport", + // `ws` must stay external so server-side WebSocket executors keep the real + // native masking implementation and optional helpers in the runtime bundle. + "ws", + "bufferutil", + "utf-8-validate", "better-sqlite3", // sqlite-vec ships a native vec0.so loaded at runtime via createRequire(). // Turbopack otherwise tries to bundle the .so and fails with "Unknown module diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index 0bee6caa424..f333db3d0ee 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -96,6 +96,7 @@ import { enforceApiKeyPolicy } from "../../shared/utils/apiKeyPolicy"; import { hasProviderQuotaBypassScope } from "../../shared/constants/apiKeyPolicyScopes"; import { cloneLogPayload } from "@/lib/logPayloads"; import { handleInternalUsageCommand } from "@/lib/usage/internalUsageCommand"; +import { shouldPersistUnavailableStateForComboFailure } from "../services/unavailableStatePolicy"; import { applyTaskAwareRouting, getTaskRoutingConfig, @@ -1742,11 +1743,12 @@ async function handleSingleModelChat( model, providerProfile, { - persistUnavailableState: !( - isCombo && - result.status === 429 && - (failureKind === "rate_limit" || failureKind === "transient") - ), + persistUnavailableState: shouldPersistUnavailableStateForComboFailure({ + isCombo, + provider, + status: result.status, + failureKind, + }), isCombo, } ); diff --git a/src/sse/services/unavailableStatePolicy.ts b/src/sse/services/unavailableStatePolicy.ts new file mode 100644 index 00000000000..80dbaff80b1 --- /dev/null +++ b/src/sse/services/unavailableStatePolicy.ts @@ -0,0 +1,22 @@ +/** + * Policy for whether a combo fallback should persist connection unavailability. + * + * Combo-managed transient 429s stay in-memory for Antigravity because that path + * has dedicated quota handling. Other providers should persist the cooldown so + * the same connection is skipped on later requests, not just within the current + * combo attempt. + */ +export function shouldPersistUnavailableStateForComboFailure(options: { + isCombo?: boolean; + provider?: string | null; + status: number; + failureKind?: string | null; +}): boolean { + const isAntigravityComboTransient429 = + options.isCombo === true && + options.provider === "antigravity" && + options.status === 429 && + (options.failureKind === "rate_limit" || options.failureKind === "transient"); + + return !isAntigravityComboTransient429; +} diff --git a/tests/unit/next-config.test.ts b/tests/unit/next-config.test.ts index 29821fa4ec1..d2a8753d010 100644 --- a/tests/unit/next-config.test.ts +++ b/tests/unit/next-config.test.ts @@ -101,6 +101,9 @@ test("next config declares Turbopack aliases, runtime assets and server external for (const packageName of [ "thread-stream", + "ws", + "bufferutil", + "utf-8-validate", "better-sqlite3", // sqlite-vec ships a native vec0.so loaded at runtime; without externalizing it // the Turbopack build fails with "Unknown module type" on the .so (issue #3066). diff --git a/tests/unit/unavailable-state-policy.test.ts b/tests/unit/unavailable-state-policy.test.ts new file mode 100644 index 00000000000..438833f320d --- /dev/null +++ b/tests/unit/unavailable-state-policy.test.ts @@ -0,0 +1,38 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { shouldPersistUnavailableStateForComboFailure } from "../../src/sse/services/unavailableStatePolicy.ts"; + +test("combo transient 429 persists for non-Antigravity providers", () => { + assert.equal( + shouldPersistUnavailableStateForComboFailure({ + isCombo: true, + provider: "openai", + status: 429, + failureKind: "transient", + }), + true + ); + + assert.equal( + shouldPersistUnavailableStateForComboFailure({ + isCombo: true, + provider: "anthropic", + status: 429, + failureKind: "rate_limit", + }), + true + ); +}); + +test("combo transient 429 stays in-memory for Antigravity", () => { + assert.equal( + shouldPersistUnavailableStateForComboFailure({ + isCombo: true, + provider: "antigravity", + status: 429, + failureKind: "transient", + }), + false + ); +});