Skip to content

Commit 991baa8

Browse files
authored
Merge d4f31fb into 4bdf224
2 parents 4bdf224 + d4f31fb commit 991baa8

4 files changed

Lines changed: 102 additions & 9 deletions

File tree

packages/ai/src/providers/google-shared.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream.js";
66
import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../utils/system-prompt-cache-boundary.js";
77
import {
88
buildGoogleGenerateContentParams,
9+
buildGoogleSimpleThinking,
910
consumeGoogleGenerateContentStream,
1011
} from "./google-shared.js";
1112

@@ -53,6 +54,30 @@ function createOutput(): AssistantMessage {
5354
};
5455
}
5556

57+
describe("buildGoogleSimpleThinking", () => {
58+
it.each(["xhigh", "max"] as const)(
59+
"keeps thinking disabled when reasoning=%s clamps to off",
60+
(reasoning) => {
61+
const offOnlyThinkingModel = {
62+
...model,
63+
id: "gemini-3-flash-preview",
64+
thinkingLevelMap: {
65+
minimal: null,
66+
low: null,
67+
medium: null,
68+
high: null,
69+
xhigh: null,
70+
max: null,
71+
},
72+
} satisfies Model<"google-generative-ai">;
73+
74+
expect(buildGoogleSimpleThinking(offOnlyThinkingModel, { reasoning })).toEqual({
75+
enabled: false,
76+
});
77+
},
78+
);
79+
});
80+
5681
async function* chunks(items: GenerateContentResponse[]) {
5782
yield* items;
5883
}

packages/ai/src/providers/google-shared.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,8 +557,11 @@ export function buildGoogleSimpleThinking<T extends GoogleApiType>(
557557
}
558558

559559
const clampedReasoning = clampThinkingLevel(model, options.reasoning);
560+
if (clampedReasoning === "off") {
561+
return { enabled: false };
562+
}
560563
const effort = (
561-
clampedReasoning === "off" || clampedReasoning === "max" ? "high" : clampedReasoning
564+
clampedReasoning === "max" ? "high" : clampedReasoning
562565
) as ClampedGoogleThinkingLevel;
563566

564567
if (

src/agents/google-simple-completion-stream.test.ts

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,16 @@ vi.mock("./custom-api-registry.js", () => ({
2424
ensureCustomApiRegistered,
2525
}));
2626

27-
const { prepareGoogleSimpleCompletionModel } = await import(
28-
"./google-simple-completion-stream.js"
29-
);
27+
const { prepareGoogleSimpleCompletionModel } = await import("./google-simple-completion-stream.js");
3028

3129
const GOOGLE_SIMPLE_COMPLETION_API = "openclaw-google-generative-ai-simple";
3230

3331
// Mirrors the provider catalog shape closely enough for wrapper registration
3432
// without pulling live Google model discovery into unit tests.
35-
function makeGoogleModel(id = "gemini-flash-latest"): Model<"google-generative-ai"> {
33+
function makeGoogleModel(
34+
id = "gemini-flash-latest",
35+
overrides: Partial<Model<"google-generative-ai">> = {},
36+
): Model<"google-generative-ai"> {
3637
return {
3738
id,
3839
name: id,
@@ -45,6 +46,7 @@ function makeGoogleModel(id = "gemini-flash-latest"): Model<"google-generative-a
4546
contextWindow: 1_000_000,
4647
maxTokens: 8192,
4748
headers: {},
49+
...overrides,
4850
};
4951
}
5052

@@ -152,4 +154,63 @@ describe("prepareGoogleSimpleCompletionModel", () => {
152154
).payload.generationConfig.thinkingConfig,
153155
).not.toHaveProperty("thinkingBudget");
154156
});
157+
158+
it.each(["xhigh", "max"] as const)(
159+
"preserves clamped-off intent in the final Gemini 3 payload for reasoning=%s",
160+
async (reasoning) => {
161+
const actual = await vi.importActual<
162+
typeof import("../plugin-sdk/provider-stream-shared.js")
163+
>("../plugin-sdk/provider-stream-shared.js");
164+
sanitizeGoogleThinkingPayload.mockImplementationOnce(actual.sanitizeGoogleThinkingPayload);
165+
streamSimple.mockImplementationOnce((_model, _context, options) => {
166+
const payload = {
167+
generationConfig: {
168+
thinkingConfig: { thinkingLevel: "MINIMAL" },
169+
},
170+
};
171+
options?.onPayload?.(payload, _model);
172+
return { content: [{ type: "text", text: "ok" }], payload };
173+
});
174+
const model = makeGoogleModel("gemini-3-flash-preview", {
175+
thinkingLevelMap: {
176+
minimal: null,
177+
low: null,
178+
medium: null,
179+
high: null,
180+
xhigh: null,
181+
max: null,
182+
},
183+
});
184+
const wrapped = prepareGoogleSimpleCompletionModel(model);
185+
const streamFn = ensureCustomApiRegistered.mock.calls[0]?.[1] as (
186+
...args: unknown[]
187+
) => unknown;
188+
189+
const result = await streamFn(wrapped, { messages: [] }, { apiKey: "key", reasoning });
190+
191+
expect(sanitizeGoogleThinkingPayload).toHaveBeenCalledWith({
192+
payload: {
193+
generationConfig: {
194+
thinkingConfig: { thinkingLevel: "MINIMAL" },
195+
},
196+
},
197+
modelId: "gemini-3-flash-preview",
198+
thinkingLevel: "off",
199+
});
200+
expect(result).toMatchObject({
201+
payload: {
202+
generationConfig: {
203+
thinkingConfig: { thinkingLevel: "MINIMAL" },
204+
},
205+
},
206+
});
207+
expect(
208+
(
209+
result as {
210+
payload: { generationConfig: { thinkingConfig: Record<string, unknown> } };
211+
}
212+
).payload.generationConfig.thinkingConfig,
213+
).not.toHaveProperty("includeThoughts");
214+
},
215+
);
155216
});

src/agents/google-simple-completion-stream.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
* This registers a patched Google stream API that keeps the normal Google
55
* backend but sanitizes unsupported thinking payload options for simple models.
66
*/
7+
import { clampThinkingLevel } from "@openclaw/ai/internal/runtime";
78
import { streamSimple } from "../llm/stream.js";
8-
import type { Api, Model } from "../llm/types.js";
9+
import type { Api, Model, ModelThinkingLevel } from "../llm/types.js";
910
import {
1011
sanitizeGoogleThinkingPayload,
1112
streamWithPayloadPatch,
@@ -20,26 +21,28 @@ const GOOGLE_SIMPLE_COMPLETION_API: Api = "openclaw-google-generative-ai-simple"
2021
const SOURCE_API: Api = "google-generative-ai";
2122

2223
function resolveGoogleSimpleThinkingLevel(
24+
model: Model,
2325
reasoning: unknown,
2426
): GoogleThinkingInputLevel | undefined {
2527
switch (reasoning) {
28+
case "adaptive":
29+
return reasoning;
2630
case "off":
2731
case "minimal":
2832
case "low":
2933
case "medium":
30-
case "adaptive":
3134
case "high":
3235
case "max":
3336
case "xhigh":
34-
return reasoning;
37+
return clampThinkingLevel(model, reasoning as ModelThinkingLevel);
3538
default:
3639
return undefined;
3740
}
3841
}
3942

4043
function buildGoogleSimpleCompletionStreamFn(): StreamFn {
4144
return (model, context, options) => {
42-
const googleModel = { ...model, api: SOURCE_API };
45+
const googleModel: Model = { ...model, api: SOURCE_API };
4346
return streamWithPayloadPatch(
4447
streamSimple as unknown as StreamFn,
4548
googleModel,
@@ -50,6 +53,7 @@ function buildGoogleSimpleCompletionStreamFn(): StreamFn {
5053
payload,
5154
modelId: model.id,
5255
thinkingLevel: resolveGoogleSimpleThinkingLevel(
56+
googleModel,
5357
(options as { reasoning?: unknown } | undefined)?.reasoning,
5458
),
5559
});

0 commit comments

Comments
 (0)