diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index 5b5a0356..bf9e8050 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -6,6 +6,8 @@ import { type GenerateContentParameters, GoogleGenAI, type Part, + type ThinkingConfig, + type ThinkingLevel, } from "@google/genai"; import { calculateCost } from "../models.js"; import type { @@ -31,6 +33,7 @@ export interface GoogleOptions extends StreamOptions { thinking?: { enabled: boolean; budgetTokens?: number; // -1 for dynamic, 0 to disable + level?: ThinkingLevel; }; } @@ -293,10 +296,13 @@ function buildParams( } if (options.thinking?.enabled && model.reasoning) { - config.thinkingConfig = { - includeThoughts: true, - ...(options.thinking.budgetTokens !== undefined && { thinkingBudget: options.thinking.budgetTokens }), - }; + const thinkingConfig: ThinkingConfig = { includeThoughts: true }; + if (options.thinking.level !== undefined) { + thinkingConfig.thinkingLevel = options.thinking.level; + } else if (options.thinking.budgetTokens !== undefined) { + thinkingConfig.thinkingBudget = options.thinking.budgetTokens; + } + config.thinkingConfig = thinkingConfig; } if (options.signal) { diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts index 715bd7cd..35246879 100644 --- a/packages/ai/src/stream.ts +++ b/packages/ai/src/stream.ts @@ -1,3 +1,4 @@ +import { ThinkingLevel } from "@google/genai"; import { type AnthropicOptions, streamAnthropic } from "./providers/anthropic.js"; import { type GoogleOptions, streamGoogle } from "./providers/google.js"; import { type OpenAICompletionsOptions, streamOpenAICompletions } from "./providers/openai-completions.js"; @@ -159,15 +160,26 @@ function mapOptionsForApi( case "google-generative-ai": { if (!options?.reasoning) return base as any; - const googleBudget = getGoogleBudget( - model as Model<"google-generative-ai">, - clampReasoning(options.reasoning)!, - ); + const googleModel = model as Model<"google-generative-ai">; + const effort = clampReasoning(options.reasoning)!; + + // Gemini 3 Pro models use thinkingLevel exclusively instead of thinkingBudget. + // https://ai.google.dev/gemini-api/docs/thinking#set-budget + if (isGemini3ProModel(googleModel)) { + return { + ...base, + thinking: { + enabled: true, + level: getGoogleThinkingLevel(effort), + }, + } satisfies GoogleOptions; + } + return { ...base, thinking: { enabled: true, - budgetTokens: googleBudget, + budgetTokens: getGoogleBudget(googleModel, effort), }, } satisfies GoogleOptions; } @@ -182,6 +194,23 @@ function mapOptionsForApi( type ClampedReasoningEffort = Exclude; +function isGemini3ProModel(model: Model<"google-generative-ai">): boolean { + // Covers gemini-3-pro, gemini-3-pro-preview, and possible other prefixed ids in the future + return model.id.includes("3-pro"); +} + +function getGoogleThinkingLevel(effort: ClampedReasoningEffort): ThinkingLevel { + // Gemini 3 Pro only supports LOW/HIGH (for now) + switch (effort) { + case "minimal": + case "low": + return ThinkingLevel.LOW; + case "medium": + case "high": + return ThinkingLevel.HIGH; + } +} + function getGoogleBudget(model: Model<"google-generative-ai">, effort: ClampedReasoningEffort): number { // See https://ai.google.dev/gemini-api/docs/thinking#set-budget if (model.id.includes("2.5-pro")) {