diff --git a/extensions/google/provider-hooks.ts b/extensions/google/provider-hooks.ts index 219d1041512..7b09c48bdde 100644 --- a/extensions/google/provider-hooks.ts +++ b/extensions/google/provider-hooks.ts @@ -4,25 +4,15 @@ import type { } from "openclaw/plugin-sdk/core"; import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared"; import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools"; -import { createGoogleThinkingStreamWrapper, isGoogleGemini3ProModel } from "./thinking-api.js"; +import { resolveGoogleThinkingProfile } from "./provider-policy.js"; +import { createGoogleThinkingStreamWrapper } from "./thinking-api.js"; export const GOOGLE_GEMINI_PROVIDER_HOOKS = { ...buildProviderReplayFamilyHooks({ family: "google-gemini", }), ...buildProviderToolCompatFamilyHooks("gemini"), - resolveThinkingProfile: ({ modelId }: ProviderDefaultThinkingPolicyContext) => - ({ - levels: isGoogleGemini3ProModel(modelId) - ? [{ id: "off" }, { id: "low" }, { id: "adaptive" }, { id: "high" }] - : [ - { id: "off" }, - { id: "minimal" }, - { id: "low" }, - { id: "medium" }, - { id: "adaptive" }, - { id: "high" }, - ], - }) satisfies ProviderThinkingProfile, + resolveThinkingProfile: (context: ProviderDefaultThinkingPolicyContext) => + resolveGoogleThinkingProfile(context) satisfies ProviderThinkingProfile | undefined, wrapStreamFn: createGoogleThinkingStreamWrapper, }; diff --git a/extensions/google/provider-policy-api.test.ts b/extensions/google/provider-policy-api.test.ts index 309bd52db90..ed289a3b1f3 100644 --- a/extensions/google/provider-policy-api.test.ts +++ b/extensions/google/provider-policy-api.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { normalizeConfig } from "./provider-policy-api.js"; +import { normalizeConfig, resolveThinkingProfile } from "./provider-policy-api.js"; describe("google provider policy public artifact", () => { it("normalizes Google provider config without loading the full provider plugin", () => { @@ -129,4 +129,21 @@ describe("google provider policy public artifact", () => { ], }); }); + + it("preserves Gemini 3 thinking levels when catalog reasoning metadata is stale", () => { + expect( + resolveThinkingProfile({ + provider: "google", + modelId: "gemini-3-flash-preview", + reasoning: false, + })?.levels, + ).toEqual([ + { id: "off" }, + { id: "minimal" }, + { id: "low" }, + { id: "medium" }, + { id: "adaptive" }, + { id: "high" }, + ]); + }); }); diff --git a/extensions/google/provider-policy-api.ts b/extensions/google/provider-policy-api.ts index 3da6b425b3a..bf9a7ef42ac 100644 --- a/extensions/google/provider-policy-api.ts +++ b/extensions/google/provider-policy-api.ts @@ -1,6 +1,11 @@ +import type { ProviderDefaultThinkingPolicyContext } from "openclaw/plugin-sdk/core"; import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-types"; -import { normalizeGoogleProviderConfig } from "./provider-policy.js"; +import { normalizeGoogleProviderConfig, resolveGoogleThinkingProfile } from "./provider-policy.js"; export function normalizeConfig(params: { provider: string; providerConfig: ModelProviderConfig }) { return normalizeGoogleProviderConfig(params.provider, params.providerConfig); } + +export function resolveThinkingProfile(context: ProviderDefaultThinkingPolicyContext) { + return resolveGoogleThinkingProfile(context); +} diff --git a/extensions/google/provider-policy.ts b/extensions/google/provider-policy.ts index 1e7e978d946..305e4c691ef 100644 --- a/extensions/google/provider-policy.ts +++ b/extensions/google/provider-policy.ts @@ -1,5 +1,10 @@ +import type { + ProviderDefaultThinkingPolicyContext, + ProviderThinkingProfile, +} from "openclaw/plugin-sdk/core"; import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-types"; import { normalizeAntigravityModelId, normalizeGoogleModelId } from "./model-id.js"; +import { isGoogleGemini3ProModel, isGoogleGemini3ThinkingLevelModel } from "./thinking-api.js"; type GoogleApiCarrier = { api?: string | null; @@ -174,3 +179,27 @@ export function normalizeGoogleProviderConfig( return nextProvider; } + +export function resolveGoogleThinkingProfile({ + modelId, + reasoning, +}: ProviderDefaultThinkingPolicyContext): ProviderThinkingProfile | undefined { + const isGemini3ThinkingModel = isGoogleGemini3ThinkingLevelModel(modelId); + if (reasoning === false && !isGemini3ThinkingModel) { + return undefined; + } + + return { + levels: isGoogleGemini3ProModel(modelId) + ? [{ id: "off" }, { id: "low" }, { id: "adaptive" }, { id: "high" }] + : [ + { id: "off" }, + { id: "minimal" }, + { id: "low" }, + { id: "medium" }, + { id: "adaptive" }, + { id: "high" }, + ], + preserveWhenCatalogReasoningFalse: isGemini3ThinkingModel || undefined, + }; +} diff --git a/src/auto-reply/thinking.test.ts b/src/auto-reply/thinking.test.ts index aa07e77e6cf..680fd9f3e64 100644 --- a/src/auto-reply/thinking.test.ts +++ b/src/auto-reply/thinking.test.ts @@ -192,6 +192,38 @@ describe("listThinkingLevels", () => { ).toBe("off"); }); + it("preserves provider-authoritative thinking profiles over stale catalog reasoning", () => { + providerRuntimeMocks.resolveProviderThinkingProfile.mockReturnValue({ + levels: [{ id: "off" }, { id: "minimal" }, { id: "low" }, { id: "medium" }], + preserveWhenCatalogReasoningFalse: true, + }); + const catalog = [ + { + provider: "google", + id: "gemini-3-flash-preview", + name: "Gemini 3 Flash Preview", + reasoning: false, + }, + ]; + + expect( + isThinkingLevelSupported({ + provider: "google", + model: "gemini-3-flash-preview", + level: "low", + catalog, + }), + ).toBe(true); + expect( + resolveSupportedThinkingLevel({ + provider: "google", + model: "gemini-3-flash-preview", + level: "low", + catalog, + }), + ).toBe("low"); + }); + it("passes catalog reasoning into provider thinking profiles for support checks", () => { providerRuntimeMocks.resolveProviderThinkingProfile.mockImplementation(({ context }) => ({ levels: diff --git a/src/auto-reply/thinking.ts b/src/auto-reply/thinking.ts index e486121f973..fa3c00e1b59 100644 --- a/src/auto-reply/thinking.ts +++ b/src/auto-reply/thinking.ts @@ -166,19 +166,22 @@ export function resolveThinkingProfile(params: { modelId: context.modelId, reasoning: context.reasoning, }; - if (context.reasoning === false) { - return buildOffOnlyThinkingProfile(); - } const pluginProfile = resolveProviderThinkingProfile({ provider: context.normalizedProvider, context: providerContext, }); if (pluginProfile) { const normalized = normalizeThinkingProfile(pluginProfile); - if (normalized.levels.length > 0) { + if ( + normalized.levels.length > 0 && + (context.reasoning !== false || pluginProfile.preserveWhenCatalogReasoningFalse === true) + ) { return normalized; } } + if (context.reasoning === false) { + return buildOffOnlyThinkingProfile(); + } const defaultLevel = resolveProviderDefaultThinkingLevel({ provider: context.normalizedProvider, diff --git a/src/plugins/provider-thinking.types.ts b/src/plugins/provider-thinking.types.ts index 8d28e3324d2..eab175e4608 100644 --- a/src/plugins/provider-thinking.types.ts +++ b/src/plugins/provider-thinking.types.ts @@ -49,4 +49,10 @@ export type ProviderThinkingLevel = { export type ProviderThinkingProfile = { levels: ProviderThinkingLevel[] | ReadonlyArray; defaultLevel?: ProviderThinkingLevelId | null; + /** + * Some bundled providers have model-specific thinking contracts that are more + * current than cached generic catalog metadata. Keep this opt-in so + * `reasoning: false` remains authoritative for ordinary catalog entries. + */ + preserveWhenCatalogReasoningFalse?: boolean; };