


















@@ -15,6 +15,7 @@ vi.mock("openclaw/plugin-sdk/provider-transport-runtime", async (importOriginal)
1515}));
16161717let buildGoogleGenerativeAiParams: typeof import("./transport-stream.js").buildGoogleGenerativeAiParams;
18+let buildGoogleGemini3FirstResponseRetryParams: typeof import("./transport-stream.js").buildGoogleGemini3FirstResponseRetryParams;
1819let createGoogleGenerativeAiTransportStreamFn: typeof import("./transport-stream.js").createGoogleGenerativeAiTransportStreamFn;
1920let createGoogleVertexTransportStreamFn: typeof import("./transport-stream.js").createGoogleVertexTransportStreamFn;
2021let hasGoogleVertexAuthorizedUserAdcSync: typeof import("./vertex-adc.js").hasGoogleVertexAuthorizedUserAdcSync;
@@ -85,10 +86,40 @@ function buildSseResponse(events: unknown[]): Response {
8586});
8687}
878889+function buildDelayedSecondSseResponse(params: {
90+first: unknown;
91+second: unknown;
92+delayMs: number;
93+}): Response {
94+const encoder = new TextEncoder();
95+const first = `data: ${JSON.stringify(params.first)}\n\n`;
96+const second = `data: ${JSON.stringify(params.second)}\n\ndata: [DONE]\n\n`;
97+let timeout: ReturnType<typeof setTimeout> | undefined;
98+const body = new ReadableStream<Uint8Array>({
99+start(controller) {
100+controller.enqueue(encoder.encode(first));
101+timeout = setTimeout(() => {
102+controller.enqueue(encoder.encode(second));
103+controller.close();
104+}, params.delayMs);
105+},
106+cancel() {
107+if (timeout) {
108+clearTimeout(timeout);
109+}
110+},
111+});
112+return new Response(body, {
113+status: 200,
114+headers: { "content-type": "text/event-stream" },
115+});
116+}
117+88118describe("google transport stream", () => {
89119beforeAll(async () => {
90120({
91121 buildGoogleGenerativeAiParams,
122+ buildGoogleGemini3FirstResponseRetryParams,
92123 createGoogleGenerativeAiTransportStreamFn,
93124 createGoogleVertexTransportStreamFn,
94125} = await import("./transport-stream.js"));
@@ -248,6 +279,124 @@ describe("google transport stream", () => {
248279});
249280});
250281282+it("builds a lean Gemini 3 first-response retry payload", () => {
283+const model = buildGeminiModel({
284+id: "gemini-3.1-pro-preview",
285+name: "Gemini 3.1 Pro Preview",
286+});
287+const retryPayload = buildGoogleGemini3FirstResponseRetryParams({
288+ model,
289+request: {
290+contents: [{ role: "user", parts: [{ text: "hello" }] }],
291+generationConfig: {
292+thinkingConfig: {
293+includeThoughts: true,
294+thinkingLevel: "HIGH",
295+},
296+},
297+},
298+});
299+300+expect(retryPayload?.generationConfig).toEqual({
301+thinkingConfig: {
302+thinkingLevel: "LOW",
303+},
304+});
305+});
306+307+it("retries Gemini 3 requests with lean thinking when the first attempt has no first response", async () => {
308+vi.stubEnv("OPENCLAW_GOOGLE_GEMINI_FIRST_RESPONSE_RETRY_MS", "10");
309+guardedFetchMock
310+.mockImplementationOnce(
311+(_url: string, init?: RequestInit) =>
312+new Promise<Response>((_resolve, reject) => {
313+init?.signal?.addEventListener("abort", () => {
314+reject(init.signal?.reason ?? new Error("aborted"));
315+});
316+}),
317+)
318+.mockResolvedValueOnce(
319+buildSseResponse([
320+{
321+candidates: [{ content: { parts: [{ text: "recovered" }] }, finishReason: "STOP" }],
322+},
323+]),
324+);
325+326+const model = buildGeminiModel({
327+id: "gemini-3.1-pro-preview",
328+name: "Gemini 3.1 Pro Preview",
329+});
330+const streamFn = createGoogleGenerativeAiTransportStreamFn();
331+const stream = await Promise.resolve(
332+streamFn(
333+model,
334+{
335+messages: [{ role: "user", content: "hello", timestamp: 0 }],
336+tools: [
337+{
338+name: "lookup",
339+description: "Look up a value",
340+parameters: {
341+type: "object",
342+properties: { q: { type: "string" } },
343+},
344+},
345+],
346+} as never,
347+{ reasoning: "high" } as never,
348+),
349+);
350+const result = await stream.result();
351+352+expect(result.content).toEqual([{ type: "text", text: "recovered" }]);
353+expect(guardedFetchMock).toHaveBeenCalledTimes(2);
354+const firstBody = JSON.parse(guardedFetchMock.mock.calls[0]?.[1]?.body as string);
355+const retryBody = JSON.parse(guardedFetchMock.mock.calls[1]?.[1]?.body as string);
356+expect(firstBody.generationConfig.thinkingConfig).toMatchObject({
357+includeThoughts: true,
358+thinkingLevel: "HIGH",
359+});
360+expect(retryBody.generationConfig.thinkingConfig).toEqual({
361+thinkingLevel: "LOW",
362+});
363+expect(retryBody.tools).toEqual(firstBody.tools);
364+});
365+366+it("keeps streaming after the first Gemini 3 chunk arrives before the retry deadline", async () => {
367+vi.stubEnv("OPENCLAW_GOOGLE_GEMINI_FIRST_RESPONSE_RETRY_MS", "10");
368+guardedFetchMock.mockResolvedValueOnce(
369+buildDelayedSecondSseResponse({
370+first: {
371+candidates: [{ content: { parts: [{ text: "first " }] } }],
372+},
373+second: {
374+candidates: [{ content: { parts: [{ text: "second" }] }, finishReason: "STOP" }],
375+},
376+delayMs: 25,
377+}),
378+);
379+380+const model = buildGeminiModel({
381+id: "gemini-3.1-pro-preview",
382+name: "Gemini 3.1 Pro Preview",
383+});
384+const streamFn = createGoogleGenerativeAiTransportStreamFn();
385+const stream = await Promise.resolve(
386+streamFn(
387+model,
388+{
389+messages: [{ role: "user", content: "hello", timestamp: 0 }],
390+} as never,
391+{ reasoning: "high" } as never,
392+),
393+);
394+const result = await stream.result();
395+396+expect(result.content).toEqual([{ type: "text", text: "first second" }]);
397+expect(guardedFetchMock).toHaveBeenCalledTimes(1);
398+});
399+251400it("uses bearer auth when the Google api key is an OAuth JSON payload", async () => {
252401guardedFetchMock.mockResolvedValueOnce(buildSseResponse([]));
253402此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。