diff --git a/packages/ai/src/providers/google-gemini-cli.ts b/packages/ai/src/providers/google-gemini-cli.ts index 1de86521b..249e30bde 100644 --- a/packages/ai/src/providers/google-gemini-cli.ts +++ b/packages/ai/src/providers/google-gemini-cli.ts @@ -1006,7 +1006,12 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = ( } const streamed = await streamResponse(currentResponse); - const acceptedSilence = options?.acceptEmptyResponse === true && !streamed.strippedPlanningLeak; + // Only accept an empty STOP as valid silence once every fallback + // endpoint is exhausted: an earlier endpoint returning empty + // successful streams must still fail over (Antigravity auto mode) + // rather than be recorded as a real silent review. + const acceptedSilence = + options?.acceptEmptyResponse === true && !streamed.strippedPlanningLeak && isLastEndpoint; if (output.stopReason !== "stop" || streamed.meaningful || acceptedSilence) { receivedContent = streamed.meaningful || acceptedSilence; break; diff --git a/packages/ai/test/google-empty-response-retry.test.ts b/packages/ai/test/google-empty-response-retry.test.ts index 6a44d1012..03f1f37d0 100644 --- a/packages/ai/test/google-empty-response-retry.test.ts +++ b/packages/ai/test/google-empty-response-retry.test.ts @@ -364,6 +364,34 @@ describe("Google empty-response retry (Cloud Code Assist path)", () => { expect(textOf(result)).toBe("Recovered."); }); + it("exhausts Antigravity auto failover before accepting silence", async () => { + const requestedEndpoints: string[] = []; + const fetchMock: FetchImpl = async input => { + const endpoint = endpointFromInput(input); + requestedEndpoints.push(endpoint); + return withResponseUrl(sse(ccaChunk("")), endpoint); + }; + + const stream = streamGoogleGeminiCli(antigravityModel, context, { + apiKey: JSON.stringify({ token: "token", projectId: "proj-123" }), + antigravityEndpointMode: "auto", + acceptEmptyResponse: true, + fetch: fetchMock, + }); + const result = await stream.result(); + + // Daily still burns its empty-response budget and fails over; only the + // last (sandbox) endpoint records the empty STOP as valid silence. + expect(requestedEndpoints).toEqual([ + ANTIGRAVITY_DAILY_ENDPOINT, + ANTIGRAVITY_DAILY_ENDPOINT, + ANTIGRAVITY_DAILY_ENDPOINT, + ANTIGRAVITY_SANDBOX_ENDPOINT, + ]); + expect(result.stopReason).toBe("stop"); + expect(result.errorMessage).toBeUndefined(); + }); + for (const { mode, endpoint } of [ { mode: "production", endpoint: ANTIGRAVITY_DAILY_ENDPOINT }, { mode: "sandbox", endpoint: ANTIGRAVITY_SANDBOX_ENDPOINT },