
























@@ -6,26 +6,34 @@ import {
66type Content,
77FinishReason,
88FunctionCallingConfigMode,
9+type GenerateContentConfig,
10+type GenerateContentParameters,
911type GenerateContentResponse,
1012type Part,
13+type ThinkingConfig,
1114} from "@google/genai";
12-import { calculateCost } from "../model-utils.js";
15+import { calculateCost, clampThinkingLevel } from "../model-utils.js";
1316import type {
17+Api,
1418AssistantMessage,
1519Context,
1620ImageContent,
1721Model,
22+SimpleStreamOptions,
1823StopReason,
1924TextContent,
25+ThinkingBudgets,
2026ThinkingContent,
27+ThinkingLevel as AgentThinkingLevel,
2128Tool,
2229ToolCall,
30+StreamOptions,
2331} from "../types.js";
2432import type { AssistantMessageEventStream } from "../utils/event-stream.js";
2533import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
2634import { transformMessages } from "./transform-messages.js";
273528-type GoogleApiType = "google-generative-ai" | "google-vertex";
36+export type GoogleApiType = "google-generative-ai" | "google-vertex";
29373038/**
3139 * Thinking level for Gemini 3 models.
@@ -38,6 +46,29 @@ export type GoogleThinkingLevel =
3846| "MEDIUM"
3947| "HIGH";
404849+export type GoogleToolChoice = "auto" | "none" | "any";
50+51+export type GoogleThinkingOptions = {
52+enabled: boolean;
53+budgetTokens?: number;
54+level?: GoogleThinkingLevel;
55+};
56+57+export type GoogleProviderOptions = StreamOptions & {
58+toolChoice?: GoogleToolChoice;
59+thinking?: GoogleThinkingOptions;
60+};
61+62+type GoogleGenerateContentClient = {
63+models: {
64+generateContentStream(
65+params: GenerateContentParameters,
66+): Promise<AsyncIterable<GenerateContentResponse>> | AsyncIterable<GenerateContentResponse>;
67+};
68+};
69+70+type ClampedGoogleThinkingLevel = Exclude<AgentThinkingLevel, "xhigh" | "max">;
71+4172/**
4273 * Determines whether a streamed Gemini `Part` should be treated as "thinking".
4374 *
@@ -369,6 +400,294 @@ export function mapToolChoice(choice: string): FunctionCallingConfigMode {
369400}
370401}
371402403+export function createGoogleAssistantOutput<T extends GoogleApiType>(
404+model: Model<T>,
405+api: Api = model.api,
406+): AssistantMessage {
407+return {
408+role: "assistant",
409+content: [],
410+ api,
411+provider: model.provider,
412+model: model.id,
413+usage: {
414+input: 0,
415+output: 0,
416+cacheRead: 0,
417+cacheWrite: 0,
418+totalTokens: 0,
419+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
420+},
421+stopReason: "stop",
422+timestamp: Date.now(),
423+};
424+}
425+426+export async function runGoogleGenerateContentLifecycle<T extends GoogleApiType>(params: {
427+stream: AssistantMessageEventStream;
428+model: Model<T>;
429+output: AssistantMessage;
430+options?: Pick<StreamOptions, "signal" | "onPayload">;
431+createClient: () => GoogleGenerateContentClient;
432+buildParams: () => GenerateContentParameters;
433+nextToolCallId: (name: string | undefined) => string;
434+}): Promise<void> {
435+const { stream, model, output, options } = params;
436+437+try {
438+const client = params.createClient();
439+let requestParams = params.buildParams();
440+const nextParams = await options?.onPayload?.(requestParams, model);
441+if (nextParams !== undefined) {
442+requestParams = nextParams as GenerateContentParameters;
443+}
444+const googleStream = await client.models.generateContentStream(requestParams);
445+await consumeGoogleGenerateContentStream({
446+chunks: googleStream,
447+ model,
448+ output,
449+ stream,
450+signal: options?.signal,
451+nextToolCallId: params.nextToolCallId,
452+});
453+} catch (error) {
454+for (const block of output.content) {
455+if ("index" in block) {
456+delete (block as { index?: number }).index;
457+}
458+}
459+output.stopReason = options?.signal?.aborted ? "aborted" : "error";
460+output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
461+stream.push({ type: "error", reason: output.stopReason, error: output });
462+stream.end();
463+}
464+}
465+466+export function buildGoogleGenerateContentParams<T extends GoogleApiType>(
467+model: Model<T>,
468+context: Context,
469+options: GoogleProviderOptions = {},
470+configHooks?: {
471+mapThinkingLevel?: (level: GoogleThinkingLevel) => ThinkingConfig["thinkingLevel"];
472+getDisabledThinkingConfig?: (model: Model<T>) => ThinkingConfig;
473+},
474+): GenerateContentParameters {
475+const contents = convertMessages(model, context);
476+477+const generationConfig: GenerateContentConfig = {};
478+if (options.temperature !== undefined) {
479+generationConfig.temperature = options.temperature;
480+}
481+if (options.maxTokens !== undefined) {
482+generationConfig.maxOutputTokens = options.maxTokens;
483+}
484+485+const config: GenerateContentConfig = {
486+ ...(Object.keys(generationConfig).length > 0 && generationConfig),
487+ ...(context.systemPrompt && { systemInstruction: sanitizeSurrogates(context.systemPrompt) }),
488+ ...(context.tools && context.tools.length > 0 && { tools: convertTools(context.tools) }),
489+};
490+491+if (context.tools && context.tools.length > 0 && options.toolChoice) {
492+config.toolConfig = {
493+functionCallingConfig: {
494+mode: mapToolChoice(options.toolChoice),
495+},
496+};
497+} else {
498+config.toolConfig = undefined;
499+}
500+501+if (options.thinking?.enabled && model.reasoning) {
502+const thinkingConfig: ThinkingConfig = { includeThoughts: true };
503+if (options.thinking.level !== undefined) {
504+thinkingConfig.thinkingLevel = configHooks?.mapThinkingLevel
505+ ? configHooks.mapThinkingLevel(options.thinking.level)
506+ : (options.thinking.level as ThinkingConfig["thinkingLevel"]);
507+} else if (options.thinking.budgetTokens !== undefined) {
508+thinkingConfig.thinkingBudget = options.thinking.budgetTokens;
509+}
510+config.thinkingConfig = thinkingConfig;
511+} else if (model.reasoning && options.thinking && !options.thinking.enabled) {
512+config.thinkingConfig = configHooks?.getDisabledThinkingConfig
513+ ? configHooks.getDisabledThinkingConfig(model)
514+ : getDisabledGoogleThinkingConfig(model);
515+}
516+517+if (options.signal) {
518+if (options.signal.aborted) {
519+throw new Error("Request aborted");
520+}
521+config.abortSignal = options.signal;
522+}
523+524+return {
525+model: model.id,
526+ contents,
527+ config,
528+};
529+}
530+531+export function buildGoogleSimpleThinking<T extends GoogleApiType>(
532+model: Model<T>,
533+options: SimpleStreamOptions | undefined,
534+config?: {
535+includeGemma4ThinkingLevel?: boolean;
536+useFlashLiteBudgets?: boolean;
537+},
538+): GoogleThinkingOptions {
539+if (!options?.reasoning) {
540+return { enabled: false };
541+}
542+543+const clampedReasoning = clampThinkingLevel(model, options.reasoning);
544+const effort = (
545+clampedReasoning === "off" || clampedReasoning === "max" ? "high" : clampedReasoning
546+) as ClampedGoogleThinkingLevel;
547+548+if (
549+isGemini3ProModel(model) ||
550+isGemini3FlashModel(model) ||
551+(config?.includeGemma4ThinkingLevel && isGemma4Model(model))
552+) {
553+return {
554+enabled: true,
555+level: getGoogleThinkingLevel(effort, model, {
556+includeGemma4: config?.includeGemma4ThinkingLevel,
557+}),
558+};
559+}
560+561+return {
562+enabled: true,
563+budgetTokens: getGoogleBudget(model, effort, options.thinkingBudgets, {
564+useFlashLiteBudgets: config?.useFlashLiteBudgets,
565+}),
566+};
567+}
568+569+export function getDisabledGoogleThinkingConfig<T extends GoogleApiType>(
570+model: Model<T>,
571+config?: {
572+includeGemma4?: boolean;
573+mapThinkingLevel?: (level: GoogleThinkingLevel) => ThinkingConfig["thinkingLevel"];
574+},
575+): ThinkingConfig {
576+const mapThinkingLevel = (level: GoogleThinkingLevel): ThinkingConfig["thinkingLevel"] =>
577+config?.mapThinkingLevel
578+ ? config.mapThinkingLevel(level)
579+ : (level as ThinkingConfig["thinkingLevel"]);
580+581+// Google docs: Gemini 3.1 Pro cannot disable thinking, and Gemini 3 Flash / Flash-Lite
582+// do not support full thinking-off either. For Gemini 3 models, use the lowest supported
583+// thinkingLevel without includeThoughts so hidden thinking remains invisible to OpenClaw.
584+if (isGemini3ProModel(model)) {
585+return { thinkingLevel: mapThinkingLevel("LOW") };
586+}
587+if (isGemini3FlashModel(model)) {
588+return { thinkingLevel: mapThinkingLevel("MINIMAL") };
589+}
590+if (config?.includeGemma4 && isGemma4Model(model)) {
591+return { thinkingLevel: mapThinkingLevel("MINIMAL") };
592+}
593+594+// Gemini 2.x supports disabling via thinkingBudget = 0.
595+return { thinkingBudget: 0 };
596+}
597+598+export function isGemma4Model<T extends GoogleApiType>(model: Model<T>): boolean {
599+return /gemma-?4/.test(model.id.toLowerCase());
600+}
601+602+export function isGemini3ProModel<T extends GoogleApiType>(model: Model<T>): boolean {
603+return /gemini-3(?:\.\d+)?-pro/.test(model.id.toLowerCase());
604+}
605+606+export function isGemini3FlashModel<T extends GoogleApiType>(model: Model<T>): boolean {
607+return /gemini-3(?:\.\d+)?-flash/.test(model.id.toLowerCase());
608+}
609+610+function getGoogleThinkingLevel<T extends GoogleApiType>(
611+effort: ClampedGoogleThinkingLevel,
612+model: Model<T>,
613+config?: { includeGemma4?: boolean },
614+): GoogleThinkingLevel {
615+if (isGemini3ProModel(model)) {
616+switch (effort) {
617+case "minimal":
618+case "low":
619+return "LOW";
620+case "medium":
621+case "high":
622+return "HIGH";
623+}
624+}
625+if (config?.includeGemma4 && isGemma4Model(model)) {
626+switch (effort) {
627+case "minimal":
628+case "low":
629+return "MINIMAL";
630+case "medium":
631+case "high":
632+return "HIGH";
633+}
634+}
635+switch (effort) {
636+case "minimal":
637+return "MINIMAL";
638+case "low":
639+return "LOW";
640+case "medium":
641+return "MEDIUM";
642+case "high":
643+return "HIGH";
644+}
645+return "HIGH";
646+}
647+648+function getGoogleBudget<T extends GoogleApiType>(
649+model: Model<T>,
650+effort: ClampedGoogleThinkingLevel,
651+customBudgets?: ThinkingBudgets,
652+config?: { useFlashLiteBudgets?: boolean },
653+): number {
654+if (customBudgets?.[effort] !== undefined) {
655+return customBudgets[effort];
656+}
657+658+if (model.id.includes("2.5-pro")) {
659+const budgets: Record<ClampedGoogleThinkingLevel, number> = {
660+minimal: 128,
661+low: 2048,
662+medium: 8192,
663+high: 32768,
664+};
665+return budgets[effort];
666+}
667+668+if (config?.useFlashLiteBudgets && model.id.includes("2.5-flash-lite")) {
669+const budgets: Record<ClampedGoogleThinkingLevel, number> = {
670+minimal: 512,
671+low: 2048,
672+medium: 8192,
673+high: 24576,
674+};
675+return budgets[effort];
676+}
677+678+if (model.id.includes("2.5-flash")) {
679+const budgets: Record<ClampedGoogleThinkingLevel, number> = {
680+minimal: 128,
681+low: 2048,
682+medium: 8192,
683+high: 24576,
684+};
685+return budgets[effort];
686+}
687+688+return -1;
689+}
690+372691/**
373692 * Map Gemini FinishReason to our StopReason.
374693 */
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。