mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:10:44 +00:00
fix: map google adaptive thinking dynamically
This commit is contained in:
@@ -82,6 +82,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/sessions: persist the runtime-resolved context budget from embedded agent runs, so Codex GPT-5.5 sessions keep the catalog/runtime context cap instead of falling back to the generic 200k status value. Fixes #71294. Thanks @tud0r.
|
||||
- Agents/tools: fail runs before model submission when explicit tool allowlists resolve to no callable tools, preventing text-only hallucinated tool results for missing tools such as plugin commands that were not registered. Fixes #71292. Thanks @steipete.
|
||||
- Agents/embedded: skip provider submission when an embedded run has no prompt, replay history, or prompt-local images, preventing empty OpenAI Responses requests from surfacing provider errors into user channels. Fixes #71130. Thanks @steipete.
|
||||
- Providers/Google: map `/think adaptive` to Gemini dynamic thinking instead of a fixed medium/high budget, using Gemini 3's provider default and Gemini 2.5's `thinkingBudget: -1`. Fixes #71316. Thanks @steipete.
|
||||
- Providers/MiniMax: keep M2.7 chat model metadata text-only so image tool requests route through `MiniMax-VL-01` instead of the Anthropic-compatible chat endpoint. Fixes #71296. Thanks @ilker-cevikkaya.
|
||||
- Discord/replies: run `message_sending` plugin hooks for Discord reply delivery, including DM targets, so plugins can transform or cancel outbound Discord replies consistently with other channels. Fixes #59350. (#71094) Thanks @wei840222.
|
||||
- Control UI/commands: carry provider-owned thinking option ids/labels in session rows and defaults so fresh sessions show and accept dynamic modes such as `adaptive`, `xhigh`, and `max`. Fixes #71269. Thanks @Young-Khalil.
|
||||
|
||||
@@ -14,7 +14,7 @@ title: "Thinking levels"
|
||||
- medium → “think harder”
|
||||
- high → “ultrathink” (max budget)
|
||||
- xhigh → “ultrathink+” (GPT-5.2+ and Codex models, plus Anthropic Claude Opus 4.7 effort)
|
||||
- adaptive → provider-managed adaptive thinking (supported for Claude 4.6 on Anthropic/Bedrock and Anthropic Claude Opus 4.7)
|
||||
- adaptive → provider-managed adaptive thinking (supported for Claude 4.6 on Anthropic/Bedrock, Anthropic Claude Opus 4.7, and Google Gemini dynamic thinking)
|
||||
- max → provider max reasoning (currently Anthropic Claude Opus 4.7)
|
||||
- `x-high`, `x_high`, `extra-high`, `extra high`, and `extra_high` map to `xhigh`.
|
||||
- `highest` maps to `high`.
|
||||
@@ -27,6 +27,7 @@ title: "Thinking levels"
|
||||
- Anthropic Claude Opus 4.7 maps `/think xhigh` to adaptive thinking plus `output_config.effort: "xhigh"`, because `/think` is a thinking directive and `xhigh` is the Opus 4.7 effort setting.
|
||||
- Anthropic Claude Opus 4.7 also exposes `/think max`; it maps to the same provider-owned max effort path.
|
||||
- OpenAI GPT models map `/think` through model-specific Responses API effort support. `/think off` sends `reasoning.effort: "none"` only when the target model supports it; otherwise OpenClaw omits the disabled reasoning payload instead of sending an unsupported value.
|
||||
- Google Gemini maps `/think adaptive` to Gemini's provider-owned dynamic thinking. Gemini 3 requests omit a fixed `thinkingLevel`, while Gemini 2.5 requests send `thinkingBudget: -1`; fixed levels still map to the closest Gemini `thinkingLevel` or budget for that model family.
|
||||
- MiniMax (`minimax/*`) on the Anthropic-compatible streaming path defaults to `thinking: { type: "disabled" }` unless you explicitly set thinking in model params or request params. This avoids leaked `reasoning_content` deltas from MiniMax's non-native Anthropic stream format.
|
||||
- Z.AI (`zai/*`) only supports binary thinking (`on`/`off`). Any non-`off` level is treated as `on` (mapped to `low`).
|
||||
- Moonshot (`moonshot/*`) maps `/think off` to `thinking: { type: "disabled" }` and any non-`off` level to `thinking: { type: "enabled" }`. When thinking is enabled, Moonshot only accepts `tool_choice` `auto|none`; OpenClaw normalizes incompatible values to `auto`.
|
||||
|
||||
@@ -180,6 +180,40 @@ describe("google provider plugin hooks", () => {
|
||||
runCase(cliProvider, "google-gemini-cli");
|
||||
});
|
||||
|
||||
it("advertises adaptive thinking for Gemini dynamic thinking", async () => {
|
||||
const { providers } = await registerProviderPlugin({
|
||||
plugin: googleProviderPlugin,
|
||||
id: "google",
|
||||
name: "Google Provider",
|
||||
});
|
||||
const provider = requireRegisteredProvider(providers, "google");
|
||||
expect(provider.resolveThinkingProfile).toBeDefined();
|
||||
const resolveThinkingProfile = provider.resolveThinkingProfile!;
|
||||
const gemini3Profile = resolveThinkingProfile({
|
||||
provider: "google",
|
||||
modelId: "gemini-3.1-pro-preview",
|
||||
} as never);
|
||||
const gemini25Profile = resolveThinkingProfile({
|
||||
provider: "google",
|
||||
modelId: "gemini-2.5-flash",
|
||||
} as never);
|
||||
|
||||
expect(gemini3Profile?.levels).toEqual([
|
||||
{ id: "off" },
|
||||
{ id: "low" },
|
||||
{ id: "adaptive" },
|
||||
{ id: "high" },
|
||||
]);
|
||||
expect(gemini25Profile?.levels).toEqual([
|
||||
{ id: "off" },
|
||||
{ id: "minimal" },
|
||||
{ id: "low" },
|
||||
{ id: "medium" },
|
||||
{ id: "adaptive" },
|
||||
{ id: "high" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("shares Gemini replay and stream hooks across Google provider variants", async () => {
|
||||
const { providers } = await registerProviderPlugin({
|
||||
plugin: googleProviderPlugin,
|
||||
|
||||
@@ -12,8 +12,15 @@ export const GOOGLE_GEMINI_PROVIDER_HOOKS = {
|
||||
resolveThinkingProfile: ({ modelId }: ProviderDefaultThinkingPolicyContext) =>
|
||||
({
|
||||
levels: isGoogleGemini3ProModel(modelId)
|
||||
? [{ id: "off" }, { id: "low" }, { id: "high" }]
|
||||
: [{ id: "off" }, { id: "minimal" }, { id: "low" }, { id: "medium" }, { id: "high" }],
|
||||
? [{ id: "off" }, { id: "low" }, { id: "adaptive" }, { id: "high" }]
|
||||
: [
|
||||
{ id: "off" },
|
||||
{ id: "minimal" },
|
||||
{ id: "low" },
|
||||
{ id: "medium" },
|
||||
{ id: "adaptive" },
|
||||
{ id: "high" },
|
||||
],
|
||||
}) satisfies ProviderThinkingProfile,
|
||||
wrapStreamFn: createGoogleThinkingStreamWrapper,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export {
|
||||
createGoogleThinkingPayloadWrapper,
|
||||
createGoogleThinkingStreamWrapper,
|
||||
isGoogleGemini25ThinkingBudgetModel,
|
||||
isGoogleGemini3FlashModel,
|
||||
isGoogleGemini3ProModel,
|
||||
isGoogleGemini3ThinkingLevelModel,
|
||||
|
||||
@@ -10,7 +10,7 @@ describe("google thinking policy", () => {
|
||||
["minimal", "LOW"],
|
||||
["low", "LOW"],
|
||||
["medium", "HIGH"],
|
||||
["adaptive", "HIGH"],
|
||||
["adaptive", undefined],
|
||||
["high", "HIGH"],
|
||||
["xhigh", "HIGH"],
|
||||
] as const)("maps Gemini 3 Pro thinking level %s to %s", (thinkingLevel, expected) => {
|
||||
@@ -40,7 +40,7 @@ describe("google thinking policy", () => {
|
||||
["minimal", "MINIMAL"],
|
||||
["low", "LOW"],
|
||||
["medium", "MEDIUM"],
|
||||
["adaptive", "MEDIUM"],
|
||||
["adaptive", undefined],
|
||||
["high", "HIGH"],
|
||||
["xhigh", "HIGH"],
|
||||
] as const)("maps Gemini 3 Flash thinking level %s to %s", (thinkingLevel, expected) => {
|
||||
@@ -53,7 +53,7 @@ describe("google thinking policy", () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
[-1, "MINIMAL"],
|
||||
[-1, undefined],
|
||||
[0, "MINIMAL"],
|
||||
[2048, "LOW"],
|
||||
[8192, "MEDIUM"],
|
||||
@@ -98,6 +98,43 @@ describe("google thinking policy", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps Gemini 3 adaptive thinking provider-dynamic instead of forcing a fixed level", () => {
|
||||
const payload = {
|
||||
generationConfig: {
|
||||
thinkingConfig: { thinkingBudget: 8192, includeThoughts: true },
|
||||
},
|
||||
};
|
||||
|
||||
sanitizeGoogleThinkingPayload({
|
||||
payload,
|
||||
modelId: "gemini-3-flash-preview",
|
||||
thinkingLevel: "adaptive",
|
||||
});
|
||||
|
||||
expect(payload.generationConfig.thinkingConfig).toEqual({
|
||||
includeThoughts: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("maps Gemini 2.5 adaptive thinking to dynamic thinkingBudget", () => {
|
||||
const payload = {
|
||||
config: {
|
||||
thinkingConfig: { thinkingBudget: 8192, includeThoughts: true },
|
||||
},
|
||||
};
|
||||
|
||||
sanitizeGoogleThinkingPayload({
|
||||
payload,
|
||||
modelId: "gemini-2.5-flash",
|
||||
thinkingLevel: "adaptive",
|
||||
});
|
||||
|
||||
expect(payload.config.thinkingConfig).toEqual({
|
||||
includeThoughts: true,
|
||||
thinkingBudget: -1,
|
||||
});
|
||||
});
|
||||
|
||||
it("maps Gemma 4 thinking mode without sending thinkingBudget", () => {
|
||||
const payload = {
|
||||
config: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export {
|
||||
createGoogleThinkingPayloadWrapper,
|
||||
createGoogleThinkingStreamWrapper,
|
||||
isGoogleGemini25ThinkingBudgetModel,
|
||||
isGoogleGemini3FlashModel,
|
||||
isGoogleGemini3ProModel,
|
||||
isGoogleGemini3ThinkingLevelModel,
|
||||
|
||||
@@ -444,6 +444,44 @@ describe("google transport stream", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps adaptive Gemini 3 thinking on provider dynamic defaults", () => {
|
||||
const params = buildGoogleGenerativeAiParams(
|
||||
buildGeminiModel({ id: "gemini-3-flash-preview" }),
|
||||
{
|
||||
messages: [{ role: "user", content: "hello", timestamp: 0 }],
|
||||
} as never,
|
||||
{
|
||||
reasoning: "adaptive",
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(params.generationConfig).toMatchObject({
|
||||
thinkingConfig: { includeThoughts: true },
|
||||
});
|
||||
expect(params.generationConfig).not.toMatchObject({
|
||||
thinkingConfig: { thinkingLevel: expect.any(String) },
|
||||
});
|
||||
expect(params.generationConfig).not.toMatchObject({
|
||||
thinkingConfig: { thinkingBudget: expect.any(Number) },
|
||||
});
|
||||
});
|
||||
|
||||
it("maps adaptive Gemini 2.5 thinking to dynamic thinkingBudget", () => {
|
||||
const params = buildGoogleGenerativeAiParams(
|
||||
buildGeminiModel({ id: "gemini-2.5-flash" }),
|
||||
{
|
||||
messages: [{ role: "user", content: "hello", timestamp: 0 }],
|
||||
} as never,
|
||||
{
|
||||
reasoning: "adaptive",
|
||||
} as never,
|
||||
);
|
||||
|
||||
expect(params.generationConfig).toMatchObject({
|
||||
thinkingConfig: { includeThoughts: true, thinkingBudget: -1 },
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes explicit Gemini 3 Pro thinking levels", () => {
|
||||
const params = buildGoogleGenerativeAiParams(
|
||||
buildGeminiModel({ id: "gemini-3.1-pro-preview" }),
|
||||
|
||||
@@ -25,6 +25,7 @@ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtim
|
||||
import { parseGeminiAuth } from "./gemini-auth.js";
|
||||
import { normalizeGoogleApiBaseUrl } from "./provider-policy.js";
|
||||
import {
|
||||
isGoogleGemini25ThinkingBudgetModel,
|
||||
isGoogleGemini3FlashModel,
|
||||
isGoogleGemini3ProModel,
|
||||
resolveGoogleGemini3ThinkingLevel,
|
||||
@@ -238,6 +239,10 @@ function getGoogleThinkingBudget(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isAdaptiveReasoningLevel(value: unknown): value is "adaptive" {
|
||||
return value === "adaptive";
|
||||
}
|
||||
|
||||
function resolveGoogleThinkingConfig(
|
||||
model: GoogleTransportModel,
|
||||
options: GoogleTransportOptions | undefined,
|
||||
@@ -268,6 +273,17 @@ function resolveGoogleThinkingConfig(
|
||||
if (!options?.reasoning) {
|
||||
return getDisabledThinkingConfig(model.id);
|
||||
}
|
||||
if (isAdaptiveReasoningLevel(options.reasoning)) {
|
||||
if (isGoogleGemini3ProModel(model.id) || isGoogleGemini3FlashModel(model.id)) {
|
||||
return { includeThoughts: true };
|
||||
}
|
||||
if (isGoogleGemini25ThinkingBudgetModel(model.id)) {
|
||||
return normalizeGoogleThinkingConfig(model.id, {
|
||||
includeThoughts: true,
|
||||
thinkingBudget: -1,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (isGoogleGemini3ProModel(model.id) || isGoogleGemini3FlashModel(model.id)) {
|
||||
return {
|
||||
includeThoughts: true,
|
||||
|
||||
@@ -97,7 +97,7 @@ describe("sanitizeGoogleThinkingPayload — gemini-2.5-pro zero budget", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("fills thinkingLevel for Gemini 3 Flash negative budgets", () => {
|
||||
it("rewrites Gemini 3 Flash negative budgets when a fixed thinking level is explicit", () => {
|
||||
const payload = {
|
||||
config: {
|
||||
thinkingConfig: { thinkingBudget: -1, includeThoughts: true },
|
||||
@@ -113,4 +113,37 @@ describe("sanitizeGoogleThinkingPayload — gemini-2.5-pro zero budget", () => {
|
||||
thinkingLevel: "MEDIUM",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps Gemini 3 adaptive thinking on provider dynamic defaults", () => {
|
||||
const payload = {
|
||||
config: {
|
||||
thinkingConfig: { thinkingBudget: 8192, includeThoughts: true },
|
||||
},
|
||||
};
|
||||
sanitizeGoogleThinkingPayload({
|
||||
payload,
|
||||
modelId: "gemini-3-flash-preview",
|
||||
thinkingLevel: "adaptive",
|
||||
});
|
||||
expect(payload.config.thinkingConfig).toEqual({
|
||||
includeThoughts: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("maps Gemini 2.5 adaptive thinking to thinkingBudget=-1", () => {
|
||||
const payload = {
|
||||
config: {
|
||||
thinkingConfig: { thinkingBudget: 8192, includeThoughts: true },
|
||||
},
|
||||
};
|
||||
sanitizeGoogleThinkingPayload({
|
||||
payload,
|
||||
modelId: "gemini-2.5-flash",
|
||||
thinkingLevel: "adaptive",
|
||||
});
|
||||
expect(payload.config.thinkingConfig).toEqual({
|
||||
includeThoughts: true,
|
||||
thinkingBudget: -1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -158,6 +158,10 @@ export function isGoogleThinkingRequiredModel(modelId: string): boolean {
|
||||
return normalizeLowercaseStringOrEmpty(modelId).includes("gemini-2.5-pro");
|
||||
}
|
||||
|
||||
export function isGoogleGemini25ThinkingBudgetModel(modelId: string): boolean {
|
||||
return /(?:^|\/)gemini-2\.5-/.test(normalizeLowercaseStringOrEmpty(modelId));
|
||||
}
|
||||
|
||||
export function isGoogleGemini3ProModel(modelId: string): boolean {
|
||||
const normalized = normalizeLowercaseStringOrEmpty(modelId);
|
||||
return /(?:^|\/)gemini-(?:3(?:\.\d+)?-pro|pro-latest)(?:-|$)/.test(normalized);
|
||||
@@ -187,15 +191,19 @@ export function resolveGoogleGemini3ThinkingLevel(params: {
|
||||
case "low":
|
||||
return "LOW";
|
||||
case "medium":
|
||||
case "adaptive":
|
||||
case "high":
|
||||
case "max":
|
||||
case "xhigh":
|
||||
return "HIGH";
|
||||
case "adaptive":
|
||||
return undefined;
|
||||
case undefined:
|
||||
break;
|
||||
}
|
||||
if (typeof params.thinkingBudget === "number") {
|
||||
if (params.thinkingBudget < 0) {
|
||||
return undefined;
|
||||
}
|
||||
return params.thinkingBudget <= 2048 ? "LOW" : "HIGH";
|
||||
}
|
||||
return undefined;
|
||||
@@ -210,18 +218,22 @@ export function resolveGoogleGemini3ThinkingLevel(params: {
|
||||
case "low":
|
||||
return "LOW";
|
||||
case "medium":
|
||||
case "adaptive":
|
||||
return "MEDIUM";
|
||||
case "high":
|
||||
case "max":
|
||||
case "xhigh":
|
||||
return "HIGH";
|
||||
case "adaptive":
|
||||
return undefined;
|
||||
case undefined:
|
||||
break;
|
||||
}
|
||||
if (typeof params.thinkingBudget !== "number") {
|
||||
return undefined;
|
||||
}
|
||||
if (params.thinkingBudget < 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (params.thinkingBudget <= 0) {
|
||||
return "MINIMAL";
|
||||
}
|
||||
@@ -355,6 +367,29 @@ function sanitizeGoogleThinkingConfigContainer(params: {
|
||||
|
||||
const thinkingBudget = thinkingConfigObj.thinkingBudget;
|
||||
|
||||
if (
|
||||
params.thinkingLevel === "adaptive" &&
|
||||
typeof params.modelId === "string" &&
|
||||
isGoogleGemini25ThinkingBudgetModel(params.modelId)
|
||||
) {
|
||||
delete thinkingConfigObj.thinkingLevel;
|
||||
thinkingConfigObj.thinkingBudget = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
params.thinkingLevel === "adaptive" &&
|
||||
typeof params.modelId === "string" &&
|
||||
isGoogleGemini3ThinkingLevelModel(params.modelId)
|
||||
) {
|
||||
delete thinkingConfigObj.thinkingBudget;
|
||||
delete thinkingConfigObj.thinkingLevel;
|
||||
if (Object.keys(thinkingConfigObj).length === 0) {
|
||||
delete configObj.thinkingConfig;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof params.modelId === "string" && isGoogleGemini3ThinkingLevelModel(params.modelId)) {
|
||||
const mappedLevel = resolveGoogleGemini3ThinkingLevel({
|
||||
modelId: params.modelId,
|
||||
|
||||
Reference in New Issue
Block a user