mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 01:31:08 +00:00
fix(google): preserve Gemma 4 thinking-off semantics (#62411) thanks @BunsDev
Co-authored-by: Nova <nova@openknot.ai>
This commit is contained in:
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Memory/wiki: restore the bundled `memory-wiki` stack with plugin, CLI, sync/query/apply tooling, and memory-host integration for wiki-backed memory workflows.
|
||||
- Providers/Arcee AI: add a bundled Arcee AI provider plugin with Trinity catalog entries, OpenRouter support, and updated onboarding/auth guidance. (#62068) Thanks @arthurbr11.
|
||||
- Providers/Google: add Gemma 4 model support and keep Google fallback resolution on the requested provider path so native Google Gemma routes work again. (#61507) Thanks @eyjohn.
|
||||
- Providers/Google: preserve explicit thinking-off semantics for Gemma 4 while still enabling Gemma reasoning support in the Google compatibility wrappers. (#62127) Thanks @romgenie, co-authored with BunsDev.
|
||||
- Providers/Anthropic: restore Claude CLI as the preferred local Anthropic path in onboarding, model-auth guidance, doctor flows, and Docker Claude CLI live lanes again.
|
||||
- ACP/ACPX plugin: bump the bundled `acpx` pin to `0.5.1` so plugin-local installs and strict version checks pick up the latest published runtime release. (#62148) Thanks @onutc.
|
||||
- Tools/media generation: auto-fallback across auth-backed image, music, and video providers by default, and remap fallback size, aspect ratio, resolution, and duration hints to the closest supported option instead of dropping intent on provider switches.
|
||||
|
||||
@@ -222,20 +222,37 @@ describe("resolveGoogleGeminiForwardCompatModel", () => {
|
||||
expect(isModernGoogleModel("gemma-3-4b-it")).toBe(true);
|
||||
});
|
||||
|
||||
it("resolves gemma model with reasoning forced off regardless of template", () => {
|
||||
it("resolves Gemma 4 models with reasoning enabled regardless of template", () => {
|
||||
const model = resolveGoogleGeminiForwardCompatModel({
|
||||
providerId: "google",
|
||||
ctx: createContext({
|
||||
provider: "google",
|
||||
modelId: "gemma-4-26b-a4b-it",
|
||||
models: [createTemplateModel("google", "gemini-3-flash-preview", { reasoning: true })],
|
||||
models: [createTemplateModel("google", "gemini-3-flash-preview", { reasoning: false })],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(model).toMatchObject({
|
||||
provider: "google",
|
||||
id: "gemma-4-26b-a4b-it",
|
||||
reasoning: false, // patch must override the template value
|
||||
reasoning: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves template reasoning for non-Gemma 4 gemma models", () => {
|
||||
const model = resolveGoogleGeminiForwardCompatModel({
|
||||
providerId: "google",
|
||||
ctx: createContext({
|
||||
provider: "google",
|
||||
modelId: "gemma-3-4b-it",
|
||||
models: [createTemplateModel("google", "gemini-3-flash-preview", { reasoning: false })],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(model).toMatchObject({
|
||||
provider: "google",
|
||||
id: "gemma-3-4b-it",
|
||||
reasoning: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -151,7 +151,9 @@ export function resolveGoogleGeminiForwardCompatModel(params: {
|
||||
googleTemplateIds: GEMMA_TEMPLATE_IDS,
|
||||
cliTemplateIds: GEMMA_TEMPLATE_IDS,
|
||||
};
|
||||
patch = { reasoning: false };
|
||||
if (lower.startsWith("gemma-4")) {
|
||||
patch = { reasoning: true };
|
||||
}
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1104,6 +1104,109 @@ describe("applyExtraParamsToAgent", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("rewrites Gemma 4 thinkingBudget to a supported Google thinkingLevel", () => {
|
||||
const payloads: Record<string, unknown>[] = [];
|
||||
const baseStreamFn: StreamFn = (_model, _context, options) => {
|
||||
const payload: Record<string, unknown> = {
|
||||
config: {
|
||||
thinkingConfig: {
|
||||
includeThoughts: true,
|
||||
thinkingBudget: 24576,
|
||||
},
|
||||
},
|
||||
};
|
||||
options?.onPayload?.(payload, _model);
|
||||
payloads.push(payload);
|
||||
return {} as ReturnType<StreamFn>;
|
||||
};
|
||||
const agent = { streamFn: baseStreamFn };
|
||||
|
||||
applyExtraParamsToAgent(agent, undefined, "google", "gemma-4-26b-a4b-it", undefined, "high");
|
||||
|
||||
const model = {
|
||||
api: "google-generative-ai",
|
||||
provider: "google",
|
||||
id: "gemma-4-26b-a4b-it",
|
||||
reasoning: true,
|
||||
} as Model<"google-generative-ai">;
|
||||
const context: Context = { messages: [] };
|
||||
void agent.streamFn?.(model, context, {});
|
||||
|
||||
expect(payloads).toHaveLength(1);
|
||||
expect(payloads[0]?.config).toEqual({
|
||||
thinkingConfig: {
|
||||
includeThoughts: true,
|
||||
thinkingLevel: "HIGH",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves Gemma 4 thinking off instead of rewriting thinkingBudget=0 to MINIMAL", () => {
|
||||
const payloads: Record<string, unknown>[] = [];
|
||||
const baseStreamFn: StreamFn = (_model, _context, options) => {
|
||||
const payload: Record<string, unknown> = {
|
||||
config: {
|
||||
thinkingConfig: {
|
||||
thinkingBudget: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
options?.onPayload?.(payload, _model);
|
||||
payloads.push(payload);
|
||||
return {} as ReturnType<StreamFn>;
|
||||
};
|
||||
const agent = { streamFn: baseStreamFn };
|
||||
|
||||
applyExtraParamsToAgent(agent, undefined, "google", "gemma-4-26b-a4b-it", undefined, "off");
|
||||
|
||||
const model = {
|
||||
api: "google-generative-ai",
|
||||
provider: "google",
|
||||
id: "gemma-4-26b-a4b-it",
|
||||
reasoning: true,
|
||||
} as Model<"google-generative-ai">;
|
||||
const context: Context = { messages: [] };
|
||||
void agent.streamFn?.(model, context, {});
|
||||
|
||||
expect(payloads).toHaveLength(1);
|
||||
expect(payloads[0]?.config).toEqual({});
|
||||
});
|
||||
|
||||
it("preserves explicit Gemma 4 thinking level when thinkingBudget=0", () => {
|
||||
const payloads: Record<string, unknown>[] = [];
|
||||
const baseStreamFn: StreamFn = (_model, _context, options) => {
|
||||
const payload: Record<string, unknown> = {
|
||||
config: {
|
||||
thinkingConfig: {
|
||||
thinkingBudget: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
options?.onPayload?.(payload, _model);
|
||||
payloads.push(payload);
|
||||
return {} as ReturnType<StreamFn>;
|
||||
};
|
||||
const agent = { streamFn: baseStreamFn };
|
||||
|
||||
applyExtraParamsToAgent(agent, undefined, "google", "gemma-4-26b-a4b-it", undefined, "high");
|
||||
|
||||
const model = {
|
||||
api: "google-generative-ai",
|
||||
provider: "google",
|
||||
id: "gemma-4-26b-a4b-it",
|
||||
reasoning: true,
|
||||
} as Model<"google-generative-ai">;
|
||||
const context: Context = { messages: [] };
|
||||
void agent.streamFn?.(model, context, {});
|
||||
|
||||
expect(payloads).toHaveLength(1);
|
||||
expect(payloads[0]?.config).toEqual({
|
||||
thinkingConfig: {
|
||||
thinkingLevel: "HIGH",
|
||||
},
|
||||
});
|
||||
});
|
||||
it("passes configured websocket transport through stream options", () => {
|
||||
const { calls, agent } = createOptionsCaptureAgent();
|
||||
const cfg = {
|
||||
|
||||
@@ -8,6 +8,10 @@ function isGemini31Model(modelId: string): boolean {
|
||||
return normalized.includes("gemini-3.1-pro") || normalized.includes("gemini-3.1-flash");
|
||||
}
|
||||
|
||||
function isGemma4Model(modelId: string): boolean {
|
||||
return modelId.trim().toLowerCase().startsWith("gemma-4");
|
||||
}
|
||||
|
||||
function mapThinkLevelToGoogleThinkingLevel(
|
||||
thinkingLevel: ThinkLevel,
|
||||
): "MINIMAL" | "LOW" | "MEDIUM" | "HIGH" | undefined {
|
||||
@@ -27,6 +31,41 @@ function mapThinkLevelToGoogleThinkingLevel(
|
||||
}
|
||||
}
|
||||
|
||||
function mapThinkLevelToGemma4ThinkingLevel(
|
||||
thinkingLevel?: ThinkLevel,
|
||||
): "MINIMAL" | "HIGH" | undefined {
|
||||
switch (thinkingLevel) {
|
||||
case "off":
|
||||
return undefined;
|
||||
case "minimal":
|
||||
case "low":
|
||||
return "MINIMAL";
|
||||
case "medium":
|
||||
case "adaptive":
|
||||
case "high":
|
||||
case "xhigh":
|
||||
return "HIGH";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeGemma4ThinkingLevel(value: unknown): "MINIMAL" | "HIGH" | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
switch (value.trim().toUpperCase()) {
|
||||
case "MINIMAL":
|
||||
case "LOW":
|
||||
return "MINIMAL";
|
||||
case "MEDIUM":
|
||||
case "HIGH":
|
||||
return "HIGH";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeGoogleThinkingPayload(params: {
|
||||
payload: unknown;
|
||||
modelId?: string;
|
||||
@@ -46,6 +85,37 @@ export function sanitizeGoogleThinkingPayload(params: {
|
||||
return;
|
||||
}
|
||||
const thinkingConfigObj = thinkingConfig as Record<string, unknown>;
|
||||
|
||||
if (typeof params.modelId === "string" && isGemma4Model(params.modelId)) {
|
||||
const normalizedThinkingLevel = normalizeGemma4ThinkingLevel(thinkingConfigObj.thinkingLevel);
|
||||
const explicitMappedLevel = mapThinkLevelToGemma4ThinkingLevel(params.thinkingLevel);
|
||||
const disabledViaBudget =
|
||||
typeof thinkingConfigObj.thinkingBudget === "number" && thinkingConfigObj.thinkingBudget <= 0;
|
||||
const hadThinkingBudget = thinkingConfigObj.thinkingBudget !== undefined;
|
||||
delete thinkingConfigObj.thinkingBudget;
|
||||
|
||||
if (
|
||||
params.thinkingLevel === "off" ||
|
||||
(disabledViaBudget && explicitMappedLevel === undefined && !normalizedThinkingLevel)
|
||||
) {
|
||||
delete thinkingConfigObj.thinkingLevel;
|
||||
if (Object.keys(thinkingConfigObj).length === 0) {
|
||||
delete configObj.thinkingConfig;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const mappedLevel =
|
||||
explicitMappedLevel ??
|
||||
normalizedThinkingLevel ??
|
||||
(hadThinkingBudget ? "MINIMAL" : undefined);
|
||||
|
||||
if (mappedLevel) {
|
||||
thinkingConfigObj.thinkingLevel = mappedLevel;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const thinkingBudget = thinkingConfigObj.thinkingBudget;
|
||||
if (typeof thinkingBudget !== "number" || thinkingBudget >= 0) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user