diff --git a/src/server/messages-native.ts b/src/server/messages-native.ts index 192a8d92d23..1c4b9fd5ac0 100644 --- a/src/server/messages-native.ts +++ b/src/server/messages-native.ts @@ -849,6 +849,17 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions) } releaseRetainedRequest(); + // The last pool account refused on spent quota and rotation found no replacement: answer the + // usage limit with its reset (#6991), as the bridge lane does, not the upstream "try again later". + const poolUsageLimit = response.status === 429 && oauthBinding + ? anthropicRoutingFor(nativeInstance!).anthropicPoolUsageLimitAfterRefusal(Date.now(), oauthBinding.routeDecision, route.modelId) + : null; + if (poolUsageLimit) { + discard(response); + cleanupAbort(); + logCtx.errorCode = "usage_limit_reached"; + return retainUpstreamMessagesRequestId(localOAuthFailure(poolUsageLimit)!, response.headers); + } if (!response.ok) { let bodyText = ""; try { diff --git a/tests/claude-integration/messages-native-oauth.test.ts b/tests/claude-integration/messages-native-oauth.test.ts index 07e980157d1..2a639c4686f 100644 --- a/tests/claude-integration/messages-native-oauth.test.ts +++ b/tests/claude-integration/messages-native-oauth.test.ts @@ -819,6 +819,33 @@ test("shared quota exhaustion is bounded and never resends an already-tried alte expect(new Set(sent.map(entry => entry.headers.get("authorization"))).size).toBe(sent.length); }); +test("a pool whose every account refuses on spent quota answers the usage limit and its reset (#6991 parity)", async () => { + await seed(2); + const config = fixtureConfig(); + config.anthropicAccountPool = { enabled: true }; + const resetSeconds = Math.floor(Date.now() / 1000) + 7_200; + config.providers.anthropic!.fetch = (async (input, init) => { + sent.push({ url: String(input), headers: new Headers(init?.headers), body: JSON.parse(String(init?.body)) }); + return Response.json({ type: "error", error: { type: "rate_limit_error", message: "fixture window spent" } }, { + status: 429, + headers: { + "anthropic-ratelimit-unified-7d-status": "rejected", "anthropic-ratelimit-unified-7d-reset": String(resetSeconds), + "retry-after": "7200", "request-id": "req_fixture_spent", + }, + }); + }) as typeof fetch; + const { response, text, row } = await send(config, { ...BODY, stream: false }); + expect(response.status).toBe(429); + expect(sent).toHaveLength(2); + expect(JSON.parse(text).error).toMatchObject({ + type: "rate_limit_error", + message: `All Anthropic OAuth accounts have reached their usage limit; the first one resets at ${new Date(resetSeconds * 1000).toISOString()}`, + }); + expect(Number(response.headers.get("retry-after"))).toBeGreaterThan(7_000); + expect(response.headers.get("request-id")).toBe("req_fixture_spent"); + expect(row.protocolTrace?.mode).toBe("native"); +}); + test("family lease releases when spend admission refuses without a physical send", async () => { await seed(1); const config = fixtureConfig();