diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 4ce694d7c38..a4ac4918c98 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -d08f0e793e66192fdbc377183ce0d94adcbec53cf334522bce8c0c457b90b0a8 plugin-sdk-api-baseline.json -924f20b350a9f1997e95b3d7249cbb6720c9576c63e6c0c15cca0164734fd93d plugin-sdk-api-baseline.jsonl +0b0cf2ecc30501bb6381671e3704570f405655b026a0b8b6437c3a5677450b9b plugin-sdk-api-baseline.json +cb72d7b5f73005280854654b51501ec82f5a2f23b7ccb915b63c6354300559d5 plugin-sdk-api-baseline.jsonl diff --git a/extensions/google/thinking.ts b/extensions/google/thinking.ts index 792ace94036..89aac457cd1 100644 --- a/extensions/google/thinking.ts +++ b/extensions/google/thinking.ts @@ -1,263 +1,13 @@ -import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry"; -import { createPayloadPatchStreamWrapper } from "openclaw/plugin-sdk/provider-stream-shared"; -import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; - -export type GoogleThinkingLevel = "MINIMAL" | "LOW" | "MEDIUM" | "HIGH"; -export type GoogleThinkingInputLevel = - | "off" - | "minimal" - | "low" - | "medium" - | "adaptive" - | "high" - | "xhigh"; - -export function isGoogleThinkingRequiredModel(modelId: string): boolean { - return normalizeLowercaseStringOrEmpty(modelId).includes("gemini-2.5-pro"); -} - -export function isGoogleGemini3ProModel(modelId: string): boolean { - const normalized = normalizeLowercaseStringOrEmpty(modelId); - return /(?:^|\/)gemini-(?:3(?:\.\d+)?-pro|pro-latest)(?:-|$)/.test(normalized); -} - -export function isGoogleGemini3FlashModel(modelId: string): boolean { - const normalized = normalizeLowercaseStringOrEmpty(modelId); - return /(?:^|\/)gemini-(?:3(?:\.\d+)?-flash|flash(?:-lite)?-latest)(?:-|$)/.test(normalized); -} - -export function isGoogleGemini3ThinkingLevelModel(modelId: string): boolean { - return isGoogleGemini3ProModel(modelId) || isGoogleGemini3FlashModel(modelId); -} - -export function resolveGoogleGemini3ThinkingLevel(params: { - modelId?: string; - thinkingLevel?: GoogleThinkingInputLevel; - thinkingBudget?: number; -}): GoogleThinkingLevel | undefined { - if (typeof params.modelId !== "string") { - return undefined; - } - if (isGoogleGemini3ProModel(params.modelId)) { - switch (params.thinkingLevel) { - case "off": - case "minimal": - case "low": - return "LOW"; - case "medium": - case "adaptive": - case "high": - case "xhigh": - return "HIGH"; - } - if (typeof params.thinkingBudget === "number") { - return params.thinkingBudget <= 2048 ? "LOW" : "HIGH"; - } - return undefined; - } - if (!isGoogleGemini3FlashModel(params.modelId)) { - return undefined; - } - switch (params.thinkingLevel) { - case "off": - case "minimal": - return "MINIMAL"; - case "low": - return "LOW"; - case "medium": - case "adaptive": - return "MEDIUM"; - case "high": - case "xhigh": - return "HIGH"; - } - if (typeof params.thinkingBudget !== "number") { - return undefined; - } - if (params.thinkingBudget <= 0) { - return "MINIMAL"; - } - if (params.thinkingBudget <= 2048) { - return "LOW"; - } - if (params.thinkingBudget <= 8192) { - return "MEDIUM"; - } - return "HIGH"; -} - -export function stripInvalidGoogleThinkingBudget(params: { - thinkingConfig: Record; - modelId?: string; -}): boolean { - if ( - params.thinkingConfig.thinkingBudget !== 0 || - typeof params.modelId !== "string" || - !isGoogleThinkingRequiredModel(params.modelId) - ) { - return false; - } - delete params.thinkingConfig.thinkingBudget; - return true; -} - -function isGemma4Model(modelId: string): boolean { - return normalizeLowercaseStringOrEmpty(modelId).startsWith("gemma-4"); -} - -function mapThinkLevelToGemma4ThinkingLevel( - thinkingLevel?: GoogleThinkingInputLevel, -): "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; - thinkingLevel?: GoogleThinkingInputLevel; -}): void { - if (!params.payload || typeof params.payload !== "object") { - return; - } - const payloadObj = params.payload as Record; - sanitizeGoogleThinkingConfigContainer({ - container: payloadObj.config, - modelId: params.modelId, - thinkingLevel: params.thinkingLevel, - }); - sanitizeGoogleThinkingConfigContainer({ - container: payloadObj.generationConfig, - modelId: params.modelId, - thinkingLevel: params.thinkingLevel, - }); -} - -function sanitizeGoogleThinkingConfigContainer(params: { - container: unknown; - modelId?: string; - thinkingLevel?: GoogleThinkingInputLevel; -}): void { - if (!params.container || typeof params.container !== "object") { - return; - } - const configObj = params.container as Record; - const thinkingConfig = configObj.thinkingConfig; - if (!thinkingConfig || typeof thinkingConfig !== "object") { - return; - } - const thinkingConfigObj = thinkingConfig as Record; - - 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 params.modelId === "string" && isGoogleGemini3ThinkingLevelModel(params.modelId)) { - const mappedLevel = resolveGoogleGemini3ThinkingLevel({ - modelId: params.modelId, - thinkingLevel: params.thinkingLevel, - thinkingBudget: typeof thinkingBudget === "number" ? thinkingBudget : undefined, - }); - delete thinkingConfigObj.thinkingBudget; - if (mappedLevel) { - thinkingConfigObj.thinkingLevel = mappedLevel; - } - if (Object.keys(thinkingConfigObj).length === 0) { - delete configObj.thinkingConfig; - } - return; - } - - if ( - stripInvalidGoogleThinkingBudget({ thinkingConfig: thinkingConfigObj, modelId: params.modelId }) - ) { - if (Object.keys(thinkingConfigObj).length === 0) { - delete configObj.thinkingConfig; - } - return; - } - - if (typeof thinkingBudget !== "number" || thinkingBudget >= 0) { - return; - } - - delete thinkingConfigObj.thinkingBudget; - if (Object.keys(thinkingConfigObj).length === 0) { - delete configObj.thinkingConfig; - } -} - -export function createGoogleThinkingPayloadWrapper( - baseStreamFn: ProviderWrapStreamFnContext["streamFn"], - thinkingLevel?: GoogleThinkingInputLevel, -): NonNullable { - return createPayloadPatchStreamWrapper(baseStreamFn, ({ payload, model }) => { - if (model.api === "google-generative-ai") { - sanitizeGoogleThinkingPayload({ - payload, - modelId: model.id, - thinkingLevel, - }); - } - }); -} - -export function createGoogleThinkingStreamWrapper( - ctx: ProviderWrapStreamFnContext, -): NonNullable { - return createGoogleThinkingPayloadWrapper(ctx.streamFn, ctx.thinkingLevel); -} +export { + createGoogleThinkingPayloadWrapper, + createGoogleThinkingStreamWrapper, + isGoogleGemini3FlashModel, + isGoogleGemini3ProModel, + isGoogleGemini3ThinkingLevelModel, + isGoogleThinkingRequiredModel, + resolveGoogleGemini3ThinkingLevel, + sanitizeGoogleThinkingPayload, + stripInvalidGoogleThinkingBudget, + type GoogleThinkingInputLevel, + type GoogleThinkingLevel, +} from "openclaw/plugin-sdk/provider-stream-shared"; diff --git a/src/agents/google-thinking-compat.ts b/src/agents/google-thinking-compat.ts index 9d8f91f7601..ecfa13a12bf 100644 --- a/src/agents/google-thinking-compat.ts +++ b/src/agents/google-thinking-compat.ts @@ -1,102 +1,10 @@ -import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; - -export type GoogleThinkingLevel = "MINIMAL" | "LOW" | "MEDIUM" | "HIGH"; -export type GoogleThinkingInputLevel = - | "off" - | "minimal" - | "low" - | "medium" - | "adaptive" - | "high" - | "xhigh"; - -// Gemini 2.5 Pro only works in thinking mode and rejects thinkingBudget=0 with -// "Budget 0 is invalid. This model only works in thinking mode." -export function isGoogleThinkingRequiredModel(modelId: string): boolean { - return normalizeLowercaseStringOrEmpty(modelId).includes("gemini-2.5-pro"); -} - -export function isGoogleGemini3ProModel(modelId: string): boolean { - const normalized = normalizeLowercaseStringOrEmpty(modelId); - return /(?:^|\/)gemini-(?:3(?:\.\d+)?-pro|pro-latest)(?:-|$)/.test(normalized); -} - -export function isGoogleGemini3FlashModel(modelId: string): boolean { - const normalized = normalizeLowercaseStringOrEmpty(modelId); - return /(?:^|\/)gemini-(?:3(?:\.\d+)?-flash|flash(?:-lite)?-latest)(?:-|$)/.test(normalized); -} - -export function isGoogleGemini3ThinkingLevelModel(modelId: string): boolean { - return isGoogleGemini3ProModel(modelId) || isGoogleGemini3FlashModel(modelId); -} - -export function resolveGoogleGemini3ThinkingLevel(params: { - modelId?: string; - thinkingLevel?: GoogleThinkingInputLevel; - thinkingBudget?: number; -}): GoogleThinkingLevel | undefined { - if (typeof params.modelId !== "string") { - return undefined; - } - if (isGoogleGemini3ProModel(params.modelId)) { - switch (params.thinkingLevel) { - case "off": - case "minimal": - case "low": - return "LOW"; - case "medium": - case "adaptive": - case "high": - case "xhigh": - return "HIGH"; - } - if (typeof params.thinkingBudget === "number") { - return params.thinkingBudget <= 2048 ? "LOW" : "HIGH"; - } - return undefined; - } - if (!isGoogleGemini3FlashModel(params.modelId)) { - return undefined; - } - switch (params.thinkingLevel) { - case "off": - case "minimal": - return "MINIMAL"; - case "low": - return "LOW"; - case "medium": - case "adaptive": - return "MEDIUM"; - case "high": - case "xhigh": - return "HIGH"; - } - if (typeof params.thinkingBudget !== "number") { - return undefined; - } - if (params.thinkingBudget <= 0) { - return "MINIMAL"; - } - if (params.thinkingBudget <= 2048) { - return "LOW"; - } - if (params.thinkingBudget <= 8192) { - return "MEDIUM"; - } - return "HIGH"; -} - -export function stripInvalidGoogleThinkingBudget(params: { - thinkingConfig: Record; - modelId?: string; -}): boolean { - if ( - params.thinkingConfig.thinkingBudget !== 0 || - typeof params.modelId !== "string" || - !isGoogleThinkingRequiredModel(params.modelId) - ) { - return false; - } - delete params.thinkingConfig.thinkingBudget; - return true; -} +export { + isGoogleGemini3FlashModel, + isGoogleGemini3ProModel, + isGoogleGemini3ThinkingLevelModel, + isGoogleThinkingRequiredModel, + resolveGoogleGemini3ThinkingLevel, + stripInvalidGoogleThinkingBudget, + type GoogleThinkingInputLevel, + type GoogleThinkingLevel, +} from "../plugin-sdk/provider-stream-shared.js"; diff --git a/src/agents/pi-embedded-runner/google-stream-wrappers.ts b/src/agents/pi-embedded-runner/google-stream-wrappers.ts index c64269a1405..64573ccfb1c 100644 --- a/src/agents/pi-embedded-runner/google-stream-wrappers.ts +++ b/src/agents/pi-embedded-runner/google-stream-wrappers.ts @@ -1,169 +1,4 @@ -import type { StreamFn } from "@mariozechner/pi-agent-core"; -import { streamSimple } from "@mariozechner/pi-ai"; -import type { ThinkLevel } from "../../auto-reply/thinking.js"; -import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; -import { - isGoogleGemini3ThinkingLevelModel, - resolveGoogleGemini3ThinkingLevel, - stripInvalidGoogleThinkingBudget, -} from "../google-thinking-compat.js"; -import { streamWithPayloadPatch } from "./stream-payload-utils.js"; - -function isGemma4Model(modelId: string): boolean { - return normalizeLowercaseStringOrEmpty(modelId).startsWith("gemma-4"); -} - -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; - thinkingLevel?: ThinkLevel; -}): void { - if (!params.payload || typeof params.payload !== "object") { - return; - } - const payloadObj = params.payload as Record; - sanitizeGoogleThinkingConfigContainer({ - container: payloadObj.config, - modelId: params.modelId, - thinkingLevel: params.thinkingLevel, - }); - sanitizeGoogleThinkingConfigContainer({ - container: payloadObj.generationConfig, - modelId: params.modelId, - thinkingLevel: params.thinkingLevel, - }); -} - -function sanitizeGoogleThinkingConfigContainer(params: { - container: unknown; - modelId?: string; - thinkingLevel?: ThinkLevel; -}): void { - if (!params.container || typeof params.container !== "object") { - return; - } - const configObj = params.container as Record; - const thinkingConfig = configObj.thinkingConfig; - if (!thinkingConfig || typeof thinkingConfig !== "object") { - return; - } - const thinkingConfigObj = thinkingConfig as Record; - - 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 params.modelId === "string" && isGoogleGemini3ThinkingLevelModel(params.modelId)) { - const mappedLevel = resolveGoogleGemini3ThinkingLevel({ - modelId: params.modelId, - thinkingLevel: params.thinkingLevel, - thinkingBudget: typeof thinkingBudget === "number" ? thinkingBudget : undefined, - }); - delete thinkingConfigObj.thinkingBudget; - if (mappedLevel) { - thinkingConfigObj.thinkingLevel = mappedLevel; - } - if (Object.keys(thinkingConfigObj).length === 0) { - delete configObj.thinkingConfig; - } - return; - } - - if ( - stripInvalidGoogleThinkingBudget({ thinkingConfig: thinkingConfigObj, modelId: params.modelId }) - ) { - if (Object.keys(thinkingConfigObj).length === 0) { - delete configObj.thinkingConfig; - } - return; - } - - if (typeof thinkingBudget !== "number" || thinkingBudget >= 0) { - return; - } - - // pi-ai can emit thinkingBudget=-1 for some Google model IDs; a negative budget - // is invalid for Google-compatible backends and can lead to malformed handling. - delete thinkingConfigObj.thinkingBudget; - if (Object.keys(thinkingConfigObj).length === 0) { - delete configObj.thinkingConfig; - } -} - -export function createGoogleThinkingPayloadWrapper( - baseStreamFn: StreamFn | undefined, - thinkingLevel?: ThinkLevel, -): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => - streamWithPayloadPatch(underlying, model, context, options, (payload) => { - if (model.api === "google-generative-ai") { - sanitizeGoogleThinkingPayload({ - payload, - modelId: model.id, - thinkingLevel, - }); - } - }); -} +export { + createGoogleThinkingPayloadWrapper, + sanitizeGoogleThinkingPayload, +} from "../../plugin-sdk/provider-stream-shared.js"; diff --git a/src/plugin-sdk/provider-stream-shared.ts b/src/plugin-sdk/provider-stream-shared.ts index ceabdcea590..8c24a32673f 100644 --- a/src/plugin-sdk/provider-stream-shared.ts +++ b/src/plugin-sdk/provider-stream-shared.ts @@ -1,6 +1,8 @@ import type { StreamFn } from "@mariozechner/pi-agent-core"; import { streamSimple } from "@mariozechner/pi-ai"; import { streamWithPayloadPatch } from "../agents/pi-embedded-runner/stream-payload-utils.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; +import type { ProviderWrapStreamFnContext } from "./plugin-entry.js"; export type ProviderStreamWrapperFactory = | ((streamFn: StreamFn | undefined) => StreamFn | undefined) @@ -147,6 +149,270 @@ export function createPayloadPatchStreamWrapper( ); } +export type GoogleThinkingLevel = "MINIMAL" | "LOW" | "MEDIUM" | "HIGH"; +export type GoogleThinkingInputLevel = + | "off" + | "minimal" + | "low" + | "medium" + | "adaptive" + | "high" + | "xhigh"; + +// Gemini 2.5 Pro only works in thinking mode and rejects thinkingBudget=0 with +// "Budget 0 is invalid. This model only works in thinking mode." +export function isGoogleThinkingRequiredModel(modelId: string): boolean { + return normalizeLowercaseStringOrEmpty(modelId).includes("gemini-2.5-pro"); +} + +export function isGoogleGemini3ProModel(modelId: string): boolean { + const normalized = normalizeLowercaseStringOrEmpty(modelId); + return /(?:^|\/)gemini-(?:3(?:\.\d+)?-pro|pro-latest)(?:-|$)/.test(normalized); +} + +export function isGoogleGemini3FlashModel(modelId: string): boolean { + const normalized = normalizeLowercaseStringOrEmpty(modelId); + return /(?:^|\/)gemini-(?:3(?:\.\d+)?-flash|flash(?:-lite)?-latest)(?:-|$)/.test(normalized); +} + +export function isGoogleGemini3ThinkingLevelModel(modelId: string): boolean { + return isGoogleGemini3ProModel(modelId) || isGoogleGemini3FlashModel(modelId); +} + +export function resolveGoogleGemini3ThinkingLevel(params: { + modelId?: string; + thinkingLevel?: GoogleThinkingInputLevel; + thinkingBudget?: number; +}): GoogleThinkingLevel | undefined { + if (typeof params.modelId !== "string") { + return undefined; + } + if (isGoogleGemini3ProModel(params.modelId)) { + switch (params.thinkingLevel) { + case "off": + case "minimal": + case "low": + return "LOW"; + case "medium": + case "adaptive": + case "high": + case "xhigh": + return "HIGH"; + } + if (typeof params.thinkingBudget === "number") { + return params.thinkingBudget <= 2048 ? "LOW" : "HIGH"; + } + return undefined; + } + if (!isGoogleGemini3FlashModel(params.modelId)) { + return undefined; + } + switch (params.thinkingLevel) { + case "off": + case "minimal": + return "MINIMAL"; + case "low": + return "LOW"; + case "medium": + case "adaptive": + return "MEDIUM"; + case "high": + case "xhigh": + return "HIGH"; + } + if (typeof params.thinkingBudget !== "number") { + return undefined; + } + if (params.thinkingBudget <= 0) { + return "MINIMAL"; + } + if (params.thinkingBudget <= 2048) { + return "LOW"; + } + if (params.thinkingBudget <= 8192) { + return "MEDIUM"; + } + return "HIGH"; +} + +export function stripInvalidGoogleThinkingBudget(params: { + thinkingConfig: Record; + modelId?: string; +}): boolean { + if ( + params.thinkingConfig.thinkingBudget !== 0 || + typeof params.modelId !== "string" || + !isGoogleThinkingRequiredModel(params.modelId) + ) { + return false; + } + delete params.thinkingConfig.thinkingBudget; + return true; +} + +function isGemma4Model(modelId: string): boolean { + return normalizeLowercaseStringOrEmpty(modelId).startsWith("gemma-4"); +} + +function mapThinkLevelToGemma4ThinkingLevel( + thinkingLevel?: GoogleThinkingInputLevel, +): "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; + thinkingLevel?: GoogleThinkingInputLevel; +}): void { + if (!params.payload || typeof params.payload !== "object") { + return; + } + const payloadObj = params.payload as Record; + sanitizeGoogleThinkingConfigContainer({ + container: payloadObj.config, + modelId: params.modelId, + thinkingLevel: params.thinkingLevel, + }); + sanitizeGoogleThinkingConfigContainer({ + container: payloadObj.generationConfig, + modelId: params.modelId, + thinkingLevel: params.thinkingLevel, + }); +} + +function sanitizeGoogleThinkingConfigContainer(params: { + container: unknown; + modelId?: string; + thinkingLevel?: GoogleThinkingInputLevel; +}): void { + if (!params.container || typeof params.container !== "object") { + return; + } + const configObj = params.container as Record; + const thinkingConfig = configObj.thinkingConfig; + if (!thinkingConfig || typeof thinkingConfig !== "object") { + return; + } + const thinkingConfigObj = thinkingConfig as Record; + + 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 params.modelId === "string" && isGoogleGemini3ThinkingLevelModel(params.modelId)) { + const mappedLevel = resolveGoogleGemini3ThinkingLevel({ + modelId: params.modelId, + thinkingLevel: params.thinkingLevel, + thinkingBudget: typeof thinkingBudget === "number" ? thinkingBudget : undefined, + }); + delete thinkingConfigObj.thinkingBudget; + if (mappedLevel) { + thinkingConfigObj.thinkingLevel = mappedLevel; + } + if (Object.keys(thinkingConfigObj).length === 0) { + delete configObj.thinkingConfig; + } + return; + } + + if ( + stripInvalidGoogleThinkingBudget({ thinkingConfig: thinkingConfigObj, modelId: params.modelId }) + ) { + if (Object.keys(thinkingConfigObj).length === 0) { + delete configObj.thinkingConfig; + } + return; + } + + if (typeof thinkingBudget !== "number" || thinkingBudget >= 0) { + return; + } + + // pi-ai can emit thinkingBudget=-1 for some Google model IDs; a negative budget + // is invalid for Google-compatible backends and can lead to malformed handling. + delete thinkingConfigObj.thinkingBudget; + if (Object.keys(thinkingConfigObj).length === 0) { + delete configObj.thinkingConfig; + } +} + +export function createGoogleThinkingPayloadWrapper( + baseStreamFn: StreamFn | undefined, + thinkingLevel?: GoogleThinkingInputLevel, +): StreamFn { + return createPayloadPatchStreamWrapper(baseStreamFn, ({ payload, model }) => { + if (model.api === "google-generative-ai") { + sanitizeGoogleThinkingPayload({ + payload, + modelId: model.id, + thinkingLevel, + }); + } + }); +} + +export function createGoogleThinkingStreamWrapper( + ctx: ProviderWrapStreamFnContext, +): NonNullable { + return createGoogleThinkingPayloadWrapper(ctx.streamFn, ctx.thinkingLevel); +} + export { applyAnthropicPayloadPolicyToParams, resolveAnthropicPayloadPolicy,