fix: preserve Google Gemini 3 cron thinking

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Neerav Makwana
2026-05-21 22:41:21 -04:00
committed by clawsweeper
parent 6bd430ee35
commit 0cfd99f5ee
7 changed files with 102 additions and 20 deletions

View File

@@ -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,
};

View File

@@ -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" },
]);
});
});

View File

@@ -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);
}

View File

@@ -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,
};
}

View File

@@ -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:

View File

@@ -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,

View File

@@ -49,4 +49,10 @@ export type ProviderThinkingLevel = {
export type ProviderThinkingProfile = {
levels: ProviderThinkingLevel[] | ReadonlyArray<ProviderThinkingLevel>;
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;
};