@tanstack/ai-gemini 0.10.1 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/adapters/text.d.ts +2 -2
- package/dist/esm/adapters/text.js +14 -7
- package/dist/esm/adapters/text.js.map +1 -1
- package/dist/esm/message-types.d.ts +13 -0
- package/dist/esm/utils/client.js +12 -8
- package/dist/esm/utils/client.js.map +1 -1
- package/package.json +5 -4
- package/src/adapters/text.ts +31 -12
- package/src/message-types.ts +14 -0
- package/src/utils/client.ts +12 -15
|
@@ -2,7 +2,7 @@ import { BaseTextAdapter, StructuredOutputOptions, StructuredOutputResult } from
|
|
|
2
2
|
import { GEMINI_MODELS, GeminiChatModelProviderOptionsByName, GeminiChatModelToolCapabilitiesByName, GeminiModelInputModalitiesByName } from '../model-meta.js';
|
|
3
3
|
import { Modality, StreamChunk, TextOptions } from '@tanstack/ai';
|
|
4
4
|
import { ExternalTextProviderOptions } from '../text/text-provider-options.js';
|
|
5
|
-
import { GeminiMessageMetadataByModality } from '../message-types.js';
|
|
5
|
+
import { GeminiMessageMetadataByModality, GeminiToolCallMetadata } from '../message-types.js';
|
|
6
6
|
import { GeminiClientConfig } from '../utils.js';
|
|
7
7
|
/**
|
|
8
8
|
* Configuration for Gemini text adapter
|
|
@@ -34,7 +34,7 @@ type ResolveToolCapabilities<TModel extends string> = TModel extends keyof Gemin
|
|
|
34
34
|
* Tree-shakeable adapter for Gemini chat/text completion functionality.
|
|
35
35
|
* Import only what you need for smaller bundle sizes.
|
|
36
36
|
*/
|
|
37
|
-
export declare class GeminiTextAdapter<TModel extends (typeof GEMINI_MODELS)[number], TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>, TInputModalities extends ReadonlyArray<Modality> = ResolveInputModalities<TModel>, TToolCapabilities extends ReadonlyArray<string> = ResolveToolCapabilities<TModel>> extends BaseTextAdapter<TModel, TProviderOptions, TInputModalities, GeminiMessageMetadataByModality, TToolCapabilities> {
|
|
37
|
+
export declare class GeminiTextAdapter<TModel extends (typeof GEMINI_MODELS)[number], TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>, TInputModalities extends ReadonlyArray<Modality> = ResolveInputModalities<TModel>, TToolCapabilities extends ReadonlyArray<string> = ResolveToolCapabilities<TModel>> extends BaseTextAdapter<TModel, TProviderOptions, TInputModalities, GeminiMessageMetadataByModality, TToolCapabilities, GeminiToolCallMetadata> {
|
|
38
38
|
readonly kind: "text";
|
|
39
39
|
readonly name: "gemini";
|
|
40
40
|
private client;
|
|
@@ -213,6 +213,7 @@ class GeminiTextAdapter extends BaseTextAdapter {
|
|
|
213
213
|
if (functionCall) {
|
|
214
214
|
const toolCallId = functionCall.id || `${functionCall.name}_${Date.now()}_${nextToolIndex}`;
|
|
215
215
|
const functionArgs = functionCall.args || {};
|
|
216
|
+
const partThoughtSignature = part.thoughtSignature || void 0;
|
|
216
217
|
let toolCallData = toolCallMap.get(toolCallId);
|
|
217
218
|
if (!toolCallData) {
|
|
218
219
|
toolCallData = {
|
|
@@ -220,10 +221,13 @@ class GeminiTextAdapter extends BaseTextAdapter {
|
|
|
220
221
|
args: typeof functionArgs === "string" ? functionArgs : JSON.stringify(functionArgs),
|
|
221
222
|
index: nextToolIndex++,
|
|
222
223
|
started: false,
|
|
223
|
-
thoughtSignature:
|
|
224
|
+
thoughtSignature: partThoughtSignature
|
|
224
225
|
};
|
|
225
226
|
toolCallMap.set(toolCallId, toolCallData);
|
|
226
227
|
} else {
|
|
228
|
+
if (!toolCallData.thoughtSignature && partThoughtSignature) {
|
|
229
|
+
toolCallData.thoughtSignature = partThoughtSignature;
|
|
230
|
+
}
|
|
227
231
|
try {
|
|
228
232
|
const existingArgs = JSON.parse(toolCallData.args);
|
|
229
233
|
const newArgs = typeof functionArgs === "string" ? JSON.parse(functionArgs) : functionArgs;
|
|
@@ -244,7 +248,7 @@ class GeminiTextAdapter extends BaseTextAdapter {
|
|
|
244
248
|
timestamp,
|
|
245
249
|
index: toolCallData.index,
|
|
246
250
|
...toolCallData.thoughtSignature && {
|
|
247
|
-
|
|
251
|
+
metadata: {
|
|
248
252
|
thoughtSignature: toolCallData.thoughtSignature
|
|
249
253
|
}
|
|
250
254
|
}
|
|
@@ -465,15 +469,18 @@ class GeminiTextAdapter extends BaseTextAdapter {
|
|
|
465
469
|
} catch {
|
|
466
470
|
parsedArgs = toolCall.function.arguments;
|
|
467
471
|
}
|
|
468
|
-
const thoughtSignature = toolCall.
|
|
469
|
-
|
|
472
|
+
const thoughtSignature = toolCall.metadata?.thoughtSignature;
|
|
473
|
+
const part = {
|
|
470
474
|
functionCall: {
|
|
471
475
|
id: toolCall.id,
|
|
472
476
|
name: toolCall.function.name,
|
|
473
|
-
args: parsedArgs
|
|
474
|
-
...thoughtSignature && { thoughtSignature }
|
|
477
|
+
args: parsedArgs
|
|
475
478
|
}
|
|
476
|
-
}
|
|
479
|
+
};
|
|
480
|
+
if (thoughtSignature) {
|
|
481
|
+
part.thoughtSignature = thoughtSignature;
|
|
482
|
+
}
|
|
483
|
+
parts.push(part);
|
|
477
484
|
}
|
|
478
485
|
}
|
|
479
486
|
if (msg.role === "tool" && msg.toolCallId) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.js","sources":["../../../src/adapters/text.ts"],"sourcesContent":["import { FinishReason } from '@google/genai'\nimport { BaseTextAdapter } from '@tanstack/ai/adapters'\nimport { convertToolsToProviderFormat } from '../tools/tool-converter'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport type {\n GEMINI_MODELS,\n GeminiChatModelProviderOptionsByName,\n GeminiChatModelToolCapabilitiesByName,\n GeminiModelInputModalitiesByName,\n} from '../model-meta'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type { InternalLogger } from '@tanstack/ai/adapter-internals'\nimport type {\n Content,\n GenerateContentParameters,\n GenerateContentResponse,\n GoogleGenAI,\n Part,\n ThinkingLevel,\n} from '@google/genai'\nimport type {\n ContentPart,\n Modality,\n ModelMessage,\n StreamChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type { ExternalTextProviderOptions } from '../text/text-provider-options'\nimport type { GeminiMessageMetadataByModality } from '../message-types'\nimport type { GeminiClientConfig } from '../utils'\n\n/** Cast an event object to StreamChunk. Adapters construct events with string\n * literal types which are structurally compatible with the EventType enum. */\nconst asChunk = (chunk: Record<string, unknown>) =>\n chunk as unknown as StreamChunk\n\n/**\n * Configuration for Gemini text adapter\n */\nexport interface GeminiTextConfig extends GeminiClientConfig {}\n\n/**\n * Gemini-specific provider options for text/chat\n */\nexport type GeminiTextProviderOptions = ExternalTextProviderOptions\n\n// ===========================\n// Type Resolution Helpers\n// ===========================\n\n/**\n * Resolve provider options for a specific model.\n * If the model has explicit options in the map, use those; otherwise use base options.\n */\ntype ResolveProviderOptions<TModel extends string> =\n TModel extends keyof GeminiChatModelProviderOptionsByName\n ? GeminiChatModelProviderOptionsByName[TModel]\n : GeminiTextProviderOptions\n\n/**\n * Resolve input modalities for a specific model.\n * If the model has explicit modalities in the map, use those; otherwise use all modalities.\n */\ntype ResolveInputModalities<TModel extends string> =\n TModel extends keyof GeminiModelInputModalitiesByName\n ? GeminiModelInputModalitiesByName[TModel]\n : readonly ['text', 'image', 'audio', 'video', 'document']\n\n/**\n * Resolve tool capabilities for a specific model.\n * If the model has explicit tools in the map, use those; otherwise use empty tuple.\n */\ntype ResolveToolCapabilities<TModel extends string> =\n TModel extends keyof GeminiChatModelToolCapabilitiesByName\n ? NonNullable<GeminiChatModelToolCapabilitiesByName[TModel]>\n : readonly []\n\n// ===========================\n// Adapter Implementation\n// ===========================\n\n/**\n * Gemini Text (Chat) Adapter\n *\n * Tree-shakeable adapter for Gemini chat/text completion functionality.\n * Import only what you need for smaller bundle sizes.\n */\nexport class GeminiTextAdapter<\n TModel extends (typeof GEMINI_MODELS)[number],\n TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>,\n TInputModalities extends ReadonlyArray<Modality> =\n ResolveInputModalities<TModel>,\n TToolCapabilities extends ReadonlyArray<string> =\n ResolveToolCapabilities<TModel>,\n> extends BaseTextAdapter<\n TModel,\n TProviderOptions,\n TInputModalities,\n GeminiMessageMetadataByModality,\n TToolCapabilities\n> {\n readonly kind = 'text' as const\n readonly name = 'gemini' as const\n\n private client: GoogleGenAI\n\n constructor(config: GeminiTextConfig, model: TModel) {\n super({}, model)\n this.client = createGeminiClient(config)\n }\n\n async *chatStream(\n options: TextOptions<GeminiTextProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n const mappedOptions = this.mapCommonOptionsToGemini(options)\n const { logger } = options\n\n try {\n logger.request(\n `activity=chat provider=gemini model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`,\n { provider: 'gemini', model: this.model },\n )\n const result =\n await this.client.models.generateContentStream(mappedOptions)\n\n yield* this.processStreamChunks(result, options, logger)\n } catch (error) {\n const timestamp = Date.now()\n logger.errors('gemini.chatStream fatal', {\n error,\n source: 'gemini.chatStream',\n })\n yield asChunk({\n type: 'RUN_ERROR',\n model: options.model,\n timestamp,\n message:\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during the chat stream.',\n error: {\n message:\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during the chat stream.',\n },\n })\n }\n }\n\n /**\n * Generate structured output using Gemini's native JSON response format.\n * Uses responseMimeType: 'application/json' and responseSchema for structured output.\n * The outputSchema is already JSON Schema (converted in the ai layer).\n */\n async structuredOutput(\n options: StructuredOutputOptions<GeminiTextProviderOptions>,\n ): Promise<StructuredOutputResult<unknown>> {\n const { chatOptions, outputSchema } = options\n const { logger } = chatOptions\n\n const mappedOptions = this.mapCommonOptionsToGemini(chatOptions)\n\n try {\n logger.request(\n `activity=chat provider=gemini model=${this.model} messages=${chatOptions.messages.length} tools=${chatOptions.tools?.length ?? 0} stream=false`,\n { provider: 'gemini', model: this.model },\n )\n // Add structured output configuration\n const result = await this.client.models.generateContent({\n ...mappedOptions,\n config: {\n ...mappedOptions.config,\n responseMimeType: 'application/json',\n responseSchema: outputSchema,\n },\n })\n\n // Extract text content from the response\n const rawText = this.extractTextFromResponse(result)\n\n // Parse the JSON response\n let parsed: unknown\n try {\n parsed = JSON.parse(rawText)\n } catch {\n throw new Error(\n `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`,\n )\n }\n\n return {\n data: parsed,\n rawText,\n }\n } catch (error) {\n logger.errors('gemini.structuredOutput fatal', {\n error,\n source: 'gemini.structuredOutput',\n })\n throw new Error(\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during structured output generation.',\n )\n }\n }\n\n /**\n * Extract text content from a non-streaming response\n */\n private extractTextFromResponse(response: GenerateContentResponse): string {\n let textContent = ''\n\n if (response.candidates?.[0]?.content?.parts) {\n for (const part of response.candidates[0].content.parts) {\n if (part.text) {\n textContent += part.text\n }\n }\n }\n\n return textContent\n }\n\n private async *processStreamChunks(\n result: AsyncGenerator<GenerateContentResponse, unknown, unknown>,\n options: TextOptions<GeminiTextProviderOptions>,\n logger: InternalLogger,\n ): AsyncIterable<StreamChunk> {\n const model = options.model\n const timestamp = Date.now()\n let accumulatedContent = ''\n let accumulatedThinking = ''\n const toolCallMap = new Map<\n string,\n {\n name: string\n args: string\n index: number\n started: boolean\n thoughtSignature?: string\n }\n >()\n let nextToolIndex = 0\n\n // AG-UI lifecycle tracking\n const runId = options.runId ?? generateId(this.name)\n const threadId = options.threadId ?? generateId(this.name)\n const messageId = generateId(this.name)\n let stepId: string | null = null\n let reasoningMessageId: string | null = null\n let hasClosedReasoning = false\n let hasEmittedRunStarted = false\n let hasEmittedTextMessageStart = false\n let hasEmittedStepStarted = false\n\n for await (const chunk of result) {\n logger.provider(`provider=gemini`, { chunk })\n // Emit RUN_STARTED on first chunk\n if (!hasEmittedRunStarted) {\n hasEmittedRunStarted = true\n yield asChunk({\n type: 'RUN_STARTED',\n runId,\n threadId,\n model,\n timestamp,\n })\n }\n\n if (chunk.candidates?.[0]?.content?.parts) {\n const parts = chunk.candidates[0].content.parts\n\n for (const part of parts) {\n if (part.text) {\n if (part.thought) {\n // Emit STEP_STARTED and REASONING events on first thinking content\n if (!hasEmittedStepStarted) {\n hasEmittedStepStarted = true\n stepId = generateId(this.name)\n reasoningMessageId = generateId(this.name)\n\n // Spec REASONING events\n yield asChunk({\n type: 'REASONING_START',\n messageId: reasoningMessageId,\n model,\n timestamp,\n })\n yield asChunk({\n type: 'REASONING_MESSAGE_START',\n messageId: reasoningMessageId,\n role: 'reasoning' as const,\n model,\n timestamp,\n })\n\n // Legacy STEP events (kept during transition)\n yield asChunk({\n type: 'STEP_STARTED',\n stepName: stepId,\n stepId,\n model,\n timestamp,\n stepType: 'thinking',\n })\n }\n\n accumulatedThinking += part.text\n\n // Spec REASONING content event\n yield asChunk({\n type: 'REASONING_MESSAGE_CONTENT',\n messageId: reasoningMessageId!,\n delta: part.text,\n model,\n timestamp,\n })\n\n // Legacy STEP event\n yield asChunk({\n type: 'STEP_FINISHED',\n stepName: stepId || generateId(this.name),\n stepId: stepId || generateId(this.name),\n model,\n timestamp,\n delta: part.text,\n content: accumulatedThinking,\n })\n } else if (part.text.trim()) {\n // Close reasoning before text starts\n if (reasoningMessageId && !hasClosedReasoning) {\n hasClosedReasoning = true\n yield asChunk({\n type: 'REASONING_MESSAGE_END',\n messageId: reasoningMessageId,\n model,\n timestamp,\n })\n yield asChunk({\n type: 'REASONING_END',\n messageId: reasoningMessageId,\n model,\n timestamp,\n })\n }\n\n // Skip whitespace-only text parts (e.g. \"\\n\" during auto-continuation)\n // Emit TEXT_MESSAGE_START on first text content\n if (!hasEmittedTextMessageStart) {\n hasEmittedTextMessageStart = true\n yield asChunk({\n type: 'TEXT_MESSAGE_START',\n messageId,\n model,\n timestamp,\n role: 'assistant',\n })\n }\n\n accumulatedContent += part.text\n yield asChunk({\n type: 'TEXT_MESSAGE_CONTENT',\n messageId,\n model,\n timestamp,\n delta: part.text,\n content: accumulatedContent,\n })\n }\n }\n\n const functionCall = part.functionCall\n if (functionCall) {\n const toolCallId =\n functionCall.id ||\n `${functionCall.name}_${Date.now()}_${nextToolIndex}`\n const functionArgs = functionCall.args || {}\n\n let toolCallData = toolCallMap.get(toolCallId)\n if (!toolCallData) {\n toolCallData = {\n name: functionCall.name || '',\n args:\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs),\n index: nextToolIndex++,\n started: false,\n thoughtSignature:\n (functionCall as any).thoughtSignature || undefined,\n }\n toolCallMap.set(toolCallId, toolCallData)\n } else {\n try {\n const existingArgs = JSON.parse(toolCallData.args)\n const newArgs =\n typeof functionArgs === 'string'\n ? JSON.parse(functionArgs)\n : functionArgs\n const mergedArgs = { ...existingArgs, ...newArgs }\n toolCallData.args = JSON.stringify(mergedArgs)\n } catch {\n toolCallData.args =\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs)\n }\n }\n\n // Emit TOOL_CALL_START if not already started\n if (!toolCallData.started) {\n toolCallData.started = true\n yield asChunk({\n type: 'TOOL_CALL_START',\n toolCallId,\n toolCallName: toolCallData.name,\n toolName: toolCallData.name,\n model,\n timestamp,\n index: toolCallData.index,\n ...(toolCallData.thoughtSignature && {\n providerMetadata: {\n thoughtSignature: toolCallData.thoughtSignature,\n },\n }),\n })\n }\n\n // Emit TOOL_CALL_ARGS\n yield asChunk({\n type: 'TOOL_CALL_ARGS',\n toolCallId,\n model,\n timestamp,\n delta: toolCallData.args,\n args: toolCallData.args,\n })\n }\n }\n } else if (chunk.data && chunk.data.trim()) {\n // Skip whitespace-only data (e.g. \"\\n\" during auto-continuation)\n // Emit TEXT_MESSAGE_START on first text content\n if (!hasEmittedTextMessageStart) {\n hasEmittedTextMessageStart = true\n yield asChunk({\n type: 'TEXT_MESSAGE_START',\n messageId,\n model,\n timestamp,\n role: 'assistant',\n })\n }\n\n accumulatedContent += chunk.data\n yield asChunk({\n type: 'TEXT_MESSAGE_CONTENT',\n messageId,\n model,\n timestamp,\n delta: chunk.data,\n content: accumulatedContent,\n })\n }\n\n if (chunk.candidates?.[0]?.finishReason) {\n const finishReason = chunk.candidates[0].finishReason\n\n if (finishReason === FinishReason.UNEXPECTED_TOOL_CALL) {\n if (chunk.candidates[0].content?.parts) {\n for (const part of chunk.candidates[0].content.parts) {\n const functionCall = part.functionCall\n if (functionCall) {\n const toolCallId =\n functionCall.id ||\n `${functionCall.name}_${Date.now()}_${nextToolIndex}`\n const functionArgs = functionCall.args || {}\n\n const argsString =\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs)\n\n toolCallMap.set(toolCallId, {\n name: functionCall.name || '',\n args: argsString,\n index: nextToolIndex++,\n started: true,\n })\n\n // Emit TOOL_CALL_START\n yield asChunk({\n type: 'TOOL_CALL_START',\n toolCallId,\n toolCallName: functionCall.name || '',\n toolName: functionCall.name || '',\n model,\n timestamp,\n index: nextToolIndex - 1,\n })\n\n // Emit TOOL_CALL_END with parsed input\n let parsedInput: unknown = {}\n try {\n const parsed =\n typeof functionArgs === 'string'\n ? JSON.parse(functionArgs)\n : functionArgs\n parsedInput =\n parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n parsedInput = {}\n }\n\n yield asChunk({\n type: 'TOOL_CALL_END',\n toolCallId,\n toolCallName: functionCall.name || '',\n toolName: functionCall.name || '',\n model,\n timestamp,\n input: parsedInput,\n })\n }\n }\n }\n }\n\n // Emit TOOL_CALL_END for all tracked tool calls\n for (const [toolCallId, toolCallData] of toolCallMap.entries()) {\n let parsedInput: unknown = {}\n try {\n const parsed = JSON.parse(toolCallData.args)\n parsedInput = parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n parsedInput = {}\n }\n\n yield asChunk({\n type: 'TOOL_CALL_END',\n toolCallId,\n toolCallName: toolCallData.name,\n toolName: toolCallData.name,\n model,\n timestamp,\n input: parsedInput,\n })\n }\n\n // Reset so a new TEXT_MESSAGE_START is emitted if text follows tool calls\n if (toolCallMap.size > 0) {\n hasEmittedTextMessageStart = false\n }\n\n if (finishReason === FinishReason.MAX_TOKENS) {\n yield asChunk({\n type: 'RUN_ERROR',\n runId,\n model,\n timestamp,\n message:\n 'The response was cut off because the maximum token limit was reached.',\n code: 'max_tokens',\n error: {\n message:\n 'The response was cut off because the maximum token limit was reached.',\n code: 'max_tokens',\n },\n })\n }\n\n // Close reasoning events if still open\n if (reasoningMessageId && !hasClosedReasoning) {\n hasClosedReasoning = true\n yield asChunk({\n type: 'REASONING_MESSAGE_END',\n messageId: reasoningMessageId,\n model,\n timestamp,\n })\n yield asChunk({\n type: 'REASONING_END',\n messageId: reasoningMessageId,\n model,\n timestamp,\n })\n }\n\n // Emit TEXT_MESSAGE_END if we had text content\n if (hasEmittedTextMessageStart) {\n yield asChunk({\n type: 'TEXT_MESSAGE_END',\n messageId,\n model,\n timestamp,\n })\n }\n\n yield asChunk({\n type: 'RUN_FINISHED',\n runId,\n threadId,\n model,\n timestamp,\n finishReason: toolCallMap.size > 0 ? 'tool_calls' : 'stop',\n usage: chunk.usageMetadata\n ? {\n promptTokens: chunk.usageMetadata.promptTokenCount ?? 0,\n completionTokens: chunk.usageMetadata.candidatesTokenCount ?? 0,\n totalTokens: chunk.usageMetadata.totalTokenCount ?? 0,\n }\n : undefined,\n })\n }\n }\n }\n\n private convertContentPartToGemini(part: ContentPart): Part {\n switch (part.type) {\n case 'text':\n return { text: part.content }\n case 'image':\n case 'audio':\n case 'video':\n case 'document': {\n if (part.source.type === 'data') {\n return {\n inlineData: {\n data: part.source.value,\n mimeType: part.source.mimeType,\n },\n }\n } else {\n // For URL sources, use provided mimeType or fall back to reasonable defaults\n const defaultMimeType = {\n image: 'image/jpeg',\n audio: 'audio/mp3',\n video: 'video/mp4',\n document: 'application/pdf',\n }[part.type]\n\n return {\n fileData: {\n fileUri: part.source.value,\n mimeType: part.source.mimeType ?? defaultMimeType,\n },\n }\n }\n }\n default: {\n const _exhaustiveCheck: never = part\n throw new Error(\n `Unsupported content part type: ${(_exhaustiveCheck as ContentPart).type}`,\n )\n }\n }\n }\n\n private formatMessages(\n messages: Array<ModelMessage>,\n ): GenerateContentParameters['contents'] {\n // Build a lookup from toolCallId → function name so functionResponse uses the\n // correct name instead of the raw call ID.\n const toolCallIdToName = new Map<string, string>()\n for (const msg of messages) {\n if (msg.role === 'assistant' && msg.toolCalls) {\n for (const tc of msg.toolCalls) {\n toolCallIdToName.set(tc.id, tc.function.name)\n }\n }\n }\n\n const formatted = messages.map((msg) => {\n const role: 'user' | 'model' = msg.role === 'assistant' ? 'model' : 'user'\n const parts: Array<Part> = []\n\n if (Array.isArray(msg.content)) {\n for (const contentPart of msg.content) {\n parts.push(this.convertContentPartToGemini(contentPart))\n }\n } else if (msg.content && msg.role !== 'tool') {\n parts.push({ text: msg.content })\n }\n\n if (msg.role === 'assistant' && msg.toolCalls?.length) {\n for (const toolCall of msg.toolCalls) {\n let parsedArgs: Record<string, unknown> = {}\n try {\n parsedArgs = toolCall.function.arguments\n ? (JSON.parse(toolCall.function.arguments) as Record<\n string,\n unknown\n >)\n : {}\n } catch {\n parsedArgs = toolCall.function.arguments as unknown as Record<\n string,\n unknown\n >\n }\n\n const thoughtSignature = toolCall.providerMetadata\n ?.thoughtSignature as string | undefined\n parts.push({\n functionCall: {\n id: toolCall.id,\n name: toolCall.function.name,\n args: parsedArgs,\n ...(thoughtSignature && { thoughtSignature }),\n } as any,\n })\n }\n }\n\n if (msg.role === 'tool' && msg.toolCallId) {\n const functionName =\n toolCallIdToName.get(msg.toolCallId) || msg.toolCallId\n parts.push({\n functionResponse: {\n id: msg.toolCallId,\n name: functionName,\n response: {\n content: msg.content || '',\n },\n } as any,\n })\n }\n\n return {\n role,\n parts: parts.length > 0 ? parts : [{ text: '' }],\n }\n })\n\n // Post-process: Gemini requires strictly alternating user/model roles.\n // Tool results are mapped to role:'user', which can create consecutive\n // user messages when followed by a new user message. Merge them.\n return this.mergeConsecutiveSameRoleMessages(formatted)\n }\n\n /**\n * Merge consecutive messages of the same role into a single message.\n * Gemini's API requires strictly alternating user/model roles.\n * Tool results are mapped to role:'user', which can collide with actual\n * user messages in multi-turn conversations.\n *\n * Also filters out empty model messages (e.g., from a previous failed request)\n * and deduplicates functionResponse parts with the same name (tool call ID).\n */\n private mergeConsecutiveSameRoleMessages(\n messages: Array<Content>,\n ): Array<Content> {\n const merged: Array<Content> = []\n\n for (const msg of messages) {\n const parts = msg.parts || []\n\n // Skip empty model messages (no parts or only empty text)\n if (msg.role === 'model') {\n const hasContent =\n parts.length > 0 &&\n !parts.every(\n (p) => 'text' in p && (p as { text: string }).text === '',\n )\n if (!hasContent) {\n continue\n }\n }\n\n const prev = merged[merged.length - 1]\n if (prev && prev.role === msg.role) {\n // Merge parts arrays\n prev.parts = [...(prev.parts || []), ...parts]\n } else {\n merged.push({ ...msg, parts: [...parts] })\n }\n }\n\n // Deduplicate functionResponse parts with the same name (tool call ID)\n for (const msg of merged) {\n if (!msg.parts) continue\n const seenFunctionResponseNames = new Set<string>()\n msg.parts = msg.parts.filter((part) => {\n if ('functionResponse' in part && part.functionResponse?.name) {\n if (seenFunctionResponseNames.has(part.functionResponse.name)) {\n return false\n }\n seenFunctionResponseNames.add(part.functionResponse.name)\n }\n return true\n })\n }\n\n return merged\n }\n\n private mapCommonOptionsToGemini(\n options: TextOptions<GeminiTextProviderOptions>,\n ) {\n const modelOpts = options.modelOptions\n const thinkingConfig = modelOpts?.thinkingConfig\n const requestOptions: GenerateContentParameters = {\n model: options.model,\n contents: this.formatMessages(options.messages),\n config: {\n ...modelOpts,\n temperature: options.temperature,\n topP: options.topP,\n maxOutputTokens: options.maxTokens,\n thinkingConfig: thinkingConfig\n ? {\n ...thinkingConfig,\n thinkingLevel: thinkingConfig.thinkingLevel\n ? // Enum is provided by the SDK, we use it for the type but cast it to string constants, here we just cast them back\n (thinkingConfig.thinkingLevel as ThinkingLevel)\n : undefined,\n }\n : undefined,\n systemInstruction: options.systemPrompts?.join('\\n'),\n tools: convertToolsToProviderFormat(options.tools),\n },\n }\n\n return requestOptions\n }\n}\n\n/**\n * Creates a Gemini text adapter with explicit API key.\n * Type resolution happens here at the call site.\n */\nexport function createGeminiChat<TModel extends (typeof GEMINI_MODELS)[number]>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiTextConfig, 'apiKey'>,\n): GeminiTextAdapter<\n TModel,\n ResolveProviderOptions<TModel>,\n ResolveInputModalities<TModel>,\n ResolveToolCapabilities<TModel>\n> {\n return new GeminiTextAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Gemini text adapter with automatic API key detection.\n * Type resolution happens here at the call site.\n */\nexport function geminiText<TModel extends (typeof GEMINI_MODELS)[number]>(\n model: TModel,\n config?: Omit<GeminiTextConfig, 'apiKey'>,\n): GeminiTextAdapter<\n TModel,\n ResolveProviderOptions<TModel>,\n ResolveInputModalities<TModel>,\n ResolveToolCapabilities<TModel>\n> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiChat(model, apiKey, config)\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"text.js","sources":["../../../src/adapters/text.ts"],"sourcesContent":["import { FinishReason } from '@google/genai'\nimport { BaseTextAdapter } from '@tanstack/ai/adapters'\nimport { convertToolsToProviderFormat } from '../tools/tool-converter'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport type {\n GEMINI_MODELS,\n GeminiChatModelProviderOptionsByName,\n GeminiChatModelToolCapabilitiesByName,\n GeminiModelInputModalitiesByName,\n} from '../model-meta'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type { InternalLogger } from '@tanstack/ai/adapter-internals'\nimport type {\n Content,\n GenerateContentParameters,\n GenerateContentResponse,\n GoogleGenAI,\n Part,\n ThinkingLevel,\n} from '@google/genai'\nimport type {\n ContentPart,\n Modality,\n ModelMessage,\n StreamChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type { ExternalTextProviderOptions } from '../text/text-provider-options'\nimport type {\n GeminiMessageMetadataByModality,\n GeminiToolCallMetadata,\n} from '../message-types'\nimport type { GeminiClientConfig } from '../utils'\n\n/** Cast an event object to StreamChunk. Adapters construct events with string\n * literal types which are structurally compatible with the EventType enum. */\nconst asChunk = (chunk: Record<string, unknown>) =>\n chunk as unknown as StreamChunk\n\n/**\n * Configuration for Gemini text adapter\n */\nexport interface GeminiTextConfig extends GeminiClientConfig {}\n\n/**\n * Gemini-specific provider options for text/chat\n */\nexport type GeminiTextProviderOptions = ExternalTextProviderOptions\n\n// ===========================\n// Type Resolution Helpers\n// ===========================\n\n/**\n * Resolve provider options for a specific model.\n * If the model has explicit options in the map, use those; otherwise use base options.\n */\ntype ResolveProviderOptions<TModel extends string> =\n TModel extends keyof GeminiChatModelProviderOptionsByName\n ? GeminiChatModelProviderOptionsByName[TModel]\n : GeminiTextProviderOptions\n\n/**\n * Resolve input modalities for a specific model.\n * If the model has explicit modalities in the map, use those; otherwise use all modalities.\n */\ntype ResolveInputModalities<TModel extends string> =\n TModel extends keyof GeminiModelInputModalitiesByName\n ? GeminiModelInputModalitiesByName[TModel]\n : readonly ['text', 'image', 'audio', 'video', 'document']\n\n/**\n * Resolve tool capabilities for a specific model.\n * If the model has explicit tools in the map, use those; otherwise use empty tuple.\n */\ntype ResolveToolCapabilities<TModel extends string> =\n TModel extends keyof GeminiChatModelToolCapabilitiesByName\n ? NonNullable<GeminiChatModelToolCapabilitiesByName[TModel]>\n : readonly []\n\n// ===========================\n// Adapter Implementation\n// ===========================\n\n/**\n * Gemini Text (Chat) Adapter\n *\n * Tree-shakeable adapter for Gemini chat/text completion functionality.\n * Import only what you need for smaller bundle sizes.\n */\nexport class GeminiTextAdapter<\n TModel extends (typeof GEMINI_MODELS)[number],\n TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>,\n TInputModalities extends ReadonlyArray<Modality> =\n ResolveInputModalities<TModel>,\n TToolCapabilities extends ReadonlyArray<string> =\n ResolveToolCapabilities<TModel>,\n> extends BaseTextAdapter<\n TModel,\n TProviderOptions,\n TInputModalities,\n GeminiMessageMetadataByModality,\n TToolCapabilities,\n GeminiToolCallMetadata\n> {\n readonly kind = 'text' as const\n readonly name = 'gemini' as const\n\n private client: GoogleGenAI\n\n constructor(config: GeminiTextConfig, model: TModel) {\n super({}, model)\n this.client = createGeminiClient(config)\n }\n\n async *chatStream(\n options: TextOptions<GeminiTextProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n const mappedOptions = this.mapCommonOptionsToGemini(options)\n const { logger } = options\n\n try {\n logger.request(\n `activity=chat provider=gemini model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`,\n { provider: 'gemini', model: this.model },\n )\n const result =\n await this.client.models.generateContentStream(mappedOptions)\n\n yield* this.processStreamChunks(result, options, logger)\n } catch (error) {\n const timestamp = Date.now()\n logger.errors('gemini.chatStream fatal', {\n error,\n source: 'gemini.chatStream',\n })\n yield asChunk({\n type: 'RUN_ERROR',\n model: options.model,\n timestamp,\n message:\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during the chat stream.',\n error: {\n message:\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during the chat stream.',\n },\n })\n }\n }\n\n /**\n * Generate structured output using Gemini's native JSON response format.\n * Uses responseMimeType: 'application/json' and responseSchema for structured output.\n * The outputSchema is already JSON Schema (converted in the ai layer).\n */\n async structuredOutput(\n options: StructuredOutputOptions<GeminiTextProviderOptions>,\n ): Promise<StructuredOutputResult<unknown>> {\n const { chatOptions, outputSchema } = options\n const { logger } = chatOptions\n\n const mappedOptions = this.mapCommonOptionsToGemini(chatOptions)\n\n try {\n logger.request(\n `activity=chat provider=gemini model=${this.model} messages=${chatOptions.messages.length} tools=${chatOptions.tools?.length ?? 0} stream=false`,\n { provider: 'gemini', model: this.model },\n )\n // Add structured output configuration\n const result = await this.client.models.generateContent({\n ...mappedOptions,\n config: {\n ...mappedOptions.config,\n responseMimeType: 'application/json',\n responseSchema: outputSchema,\n },\n })\n\n // Extract text content from the response\n const rawText = this.extractTextFromResponse(result)\n\n // Parse the JSON response\n let parsed: unknown\n try {\n parsed = JSON.parse(rawText)\n } catch {\n throw new Error(\n `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`,\n )\n }\n\n return {\n data: parsed,\n rawText,\n }\n } catch (error) {\n logger.errors('gemini.structuredOutput fatal', {\n error,\n source: 'gemini.structuredOutput',\n })\n throw new Error(\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during structured output generation.',\n )\n }\n }\n\n /**\n * Extract text content from a non-streaming response\n */\n private extractTextFromResponse(response: GenerateContentResponse): string {\n let textContent = ''\n\n if (response.candidates?.[0]?.content?.parts) {\n for (const part of response.candidates[0].content.parts) {\n if (part.text) {\n textContent += part.text\n }\n }\n }\n\n return textContent\n }\n\n private async *processStreamChunks(\n result: AsyncGenerator<GenerateContentResponse, unknown, unknown>,\n options: TextOptions<GeminiTextProviderOptions>,\n logger: InternalLogger,\n ): AsyncIterable<StreamChunk> {\n const model = options.model\n const timestamp = Date.now()\n let accumulatedContent = ''\n let accumulatedThinking = ''\n const toolCallMap = new Map<\n string,\n {\n name: string\n args: string\n index: number\n started: boolean\n thoughtSignature?: string\n }\n >()\n let nextToolIndex = 0\n\n // AG-UI lifecycle tracking\n const runId = options.runId ?? generateId(this.name)\n const threadId = options.threadId ?? generateId(this.name)\n const messageId = generateId(this.name)\n let stepId: string | null = null\n let reasoningMessageId: string | null = null\n let hasClosedReasoning = false\n let hasEmittedRunStarted = false\n let hasEmittedTextMessageStart = false\n let hasEmittedStepStarted = false\n\n for await (const chunk of result) {\n logger.provider(`provider=gemini`, { chunk })\n // Emit RUN_STARTED on first chunk\n if (!hasEmittedRunStarted) {\n hasEmittedRunStarted = true\n yield asChunk({\n type: 'RUN_STARTED',\n runId,\n threadId,\n model,\n timestamp,\n })\n }\n\n if (chunk.candidates?.[0]?.content?.parts) {\n const parts = chunk.candidates[0].content.parts\n\n for (const part of parts) {\n if (part.text) {\n if (part.thought) {\n // Emit STEP_STARTED and REASONING events on first thinking content\n if (!hasEmittedStepStarted) {\n hasEmittedStepStarted = true\n stepId = generateId(this.name)\n reasoningMessageId = generateId(this.name)\n\n // Spec REASONING events\n yield asChunk({\n type: 'REASONING_START',\n messageId: reasoningMessageId,\n model,\n timestamp,\n })\n yield asChunk({\n type: 'REASONING_MESSAGE_START',\n messageId: reasoningMessageId,\n role: 'reasoning' as const,\n model,\n timestamp,\n })\n\n // Legacy STEP events (kept during transition)\n yield asChunk({\n type: 'STEP_STARTED',\n stepName: stepId,\n stepId,\n model,\n timestamp,\n stepType: 'thinking',\n })\n }\n\n accumulatedThinking += part.text\n\n // Spec REASONING content event\n yield asChunk({\n type: 'REASONING_MESSAGE_CONTENT',\n messageId: reasoningMessageId!,\n delta: part.text,\n model,\n timestamp,\n })\n\n // Legacy STEP event\n yield asChunk({\n type: 'STEP_FINISHED',\n stepName: stepId || generateId(this.name),\n stepId: stepId || generateId(this.name),\n model,\n timestamp,\n delta: part.text,\n content: accumulatedThinking,\n })\n } else if (part.text.trim()) {\n // Close reasoning before text starts\n if (reasoningMessageId && !hasClosedReasoning) {\n hasClosedReasoning = true\n yield asChunk({\n type: 'REASONING_MESSAGE_END',\n messageId: reasoningMessageId,\n model,\n timestamp,\n })\n yield asChunk({\n type: 'REASONING_END',\n messageId: reasoningMessageId,\n model,\n timestamp,\n })\n }\n\n // Skip whitespace-only text parts (e.g. \"\\n\" during auto-continuation)\n // Emit TEXT_MESSAGE_START on first text content\n if (!hasEmittedTextMessageStart) {\n hasEmittedTextMessageStart = true\n yield asChunk({\n type: 'TEXT_MESSAGE_START',\n messageId,\n model,\n timestamp,\n role: 'assistant',\n })\n }\n\n accumulatedContent += part.text\n yield asChunk({\n type: 'TEXT_MESSAGE_CONTENT',\n messageId,\n model,\n timestamp,\n delta: part.text,\n content: accumulatedContent,\n })\n }\n }\n\n const functionCall = part.functionCall\n if (functionCall) {\n const toolCallId =\n functionCall.id ||\n `${functionCall.name}_${Date.now()}_${nextToolIndex}`\n const functionArgs = functionCall.args || {}\n\n // Gemini emits thoughtSignature as a Part-level sibling of\n // functionCall (per @google/genai Part type), not nested inside\n // functionCall itself.\n const partThoughtSignature = part.thoughtSignature || undefined\n\n let toolCallData = toolCallMap.get(toolCallId)\n if (!toolCallData) {\n toolCallData = {\n name: functionCall.name || '',\n args:\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs),\n index: nextToolIndex++,\n started: false,\n thoughtSignature: partThoughtSignature,\n }\n toolCallMap.set(toolCallId, toolCallData)\n } else {\n if (!toolCallData.thoughtSignature && partThoughtSignature) {\n toolCallData.thoughtSignature = partThoughtSignature\n }\n try {\n const existingArgs = JSON.parse(toolCallData.args)\n const newArgs =\n typeof functionArgs === 'string'\n ? JSON.parse(functionArgs)\n : functionArgs\n const mergedArgs = { ...existingArgs, ...newArgs }\n toolCallData.args = JSON.stringify(mergedArgs)\n } catch {\n toolCallData.args =\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs)\n }\n }\n\n // Emit TOOL_CALL_START if not already started\n if (!toolCallData.started) {\n toolCallData.started = true\n yield asChunk({\n type: 'TOOL_CALL_START',\n toolCallId,\n toolCallName: toolCallData.name,\n toolName: toolCallData.name,\n model,\n timestamp,\n index: toolCallData.index,\n ...(toolCallData.thoughtSignature && {\n metadata: {\n thoughtSignature: toolCallData.thoughtSignature,\n } satisfies GeminiToolCallMetadata,\n }),\n })\n }\n\n // Emit TOOL_CALL_ARGS\n yield asChunk({\n type: 'TOOL_CALL_ARGS',\n toolCallId,\n model,\n timestamp,\n delta: toolCallData.args,\n args: toolCallData.args,\n })\n }\n }\n } else if (chunk.data && chunk.data.trim()) {\n // Skip whitespace-only data (e.g. \"\\n\" during auto-continuation)\n // Emit TEXT_MESSAGE_START on first text content\n if (!hasEmittedTextMessageStart) {\n hasEmittedTextMessageStart = true\n yield asChunk({\n type: 'TEXT_MESSAGE_START',\n messageId,\n model,\n timestamp,\n role: 'assistant',\n })\n }\n\n accumulatedContent += chunk.data\n yield asChunk({\n type: 'TEXT_MESSAGE_CONTENT',\n messageId,\n model,\n timestamp,\n delta: chunk.data,\n content: accumulatedContent,\n })\n }\n\n if (chunk.candidates?.[0]?.finishReason) {\n const finishReason = chunk.candidates[0].finishReason\n\n if (finishReason === FinishReason.UNEXPECTED_TOOL_CALL) {\n if (chunk.candidates[0].content?.parts) {\n for (const part of chunk.candidates[0].content.parts) {\n const functionCall = part.functionCall\n if (functionCall) {\n const toolCallId =\n functionCall.id ||\n `${functionCall.name}_${Date.now()}_${nextToolIndex}`\n const functionArgs = functionCall.args || {}\n\n const argsString =\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs)\n\n toolCallMap.set(toolCallId, {\n name: functionCall.name || '',\n args: argsString,\n index: nextToolIndex++,\n started: true,\n })\n\n // Emit TOOL_CALL_START\n yield asChunk({\n type: 'TOOL_CALL_START',\n toolCallId,\n toolCallName: functionCall.name || '',\n toolName: functionCall.name || '',\n model,\n timestamp,\n index: nextToolIndex - 1,\n })\n\n // Emit TOOL_CALL_END with parsed input\n let parsedInput: unknown = {}\n try {\n const parsed =\n typeof functionArgs === 'string'\n ? JSON.parse(functionArgs)\n : functionArgs\n parsedInput =\n parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n parsedInput = {}\n }\n\n yield asChunk({\n type: 'TOOL_CALL_END',\n toolCallId,\n toolCallName: functionCall.name || '',\n toolName: functionCall.name || '',\n model,\n timestamp,\n input: parsedInput,\n })\n }\n }\n }\n }\n\n // Emit TOOL_CALL_END for all tracked tool calls\n for (const [toolCallId, toolCallData] of toolCallMap.entries()) {\n let parsedInput: unknown = {}\n try {\n const parsed = JSON.parse(toolCallData.args)\n parsedInput = parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n parsedInput = {}\n }\n\n yield asChunk({\n type: 'TOOL_CALL_END',\n toolCallId,\n toolCallName: toolCallData.name,\n toolName: toolCallData.name,\n model,\n timestamp,\n input: parsedInput,\n })\n }\n\n // Reset so a new TEXT_MESSAGE_START is emitted if text follows tool calls\n if (toolCallMap.size > 0) {\n hasEmittedTextMessageStart = false\n }\n\n if (finishReason === FinishReason.MAX_TOKENS) {\n yield asChunk({\n type: 'RUN_ERROR',\n runId,\n model,\n timestamp,\n message:\n 'The response was cut off because the maximum token limit was reached.',\n code: 'max_tokens',\n error: {\n message:\n 'The response was cut off because the maximum token limit was reached.',\n code: 'max_tokens',\n },\n })\n }\n\n // Close reasoning events if still open\n if (reasoningMessageId && !hasClosedReasoning) {\n hasClosedReasoning = true\n yield asChunk({\n type: 'REASONING_MESSAGE_END',\n messageId: reasoningMessageId,\n model,\n timestamp,\n })\n yield asChunk({\n type: 'REASONING_END',\n messageId: reasoningMessageId,\n model,\n timestamp,\n })\n }\n\n // Emit TEXT_MESSAGE_END if we had text content\n if (hasEmittedTextMessageStart) {\n yield asChunk({\n type: 'TEXT_MESSAGE_END',\n messageId,\n model,\n timestamp,\n })\n }\n\n yield asChunk({\n type: 'RUN_FINISHED',\n runId,\n threadId,\n model,\n timestamp,\n finishReason: toolCallMap.size > 0 ? 'tool_calls' : 'stop',\n usage: chunk.usageMetadata\n ? {\n promptTokens: chunk.usageMetadata.promptTokenCount ?? 0,\n completionTokens: chunk.usageMetadata.candidatesTokenCount ?? 0,\n totalTokens: chunk.usageMetadata.totalTokenCount ?? 0,\n }\n : undefined,\n })\n }\n }\n }\n\n private convertContentPartToGemini(part: ContentPart): Part {\n switch (part.type) {\n case 'text':\n return { text: part.content }\n case 'image':\n case 'audio':\n case 'video':\n case 'document': {\n if (part.source.type === 'data') {\n return {\n inlineData: {\n data: part.source.value,\n mimeType: part.source.mimeType,\n },\n }\n } else {\n // For URL sources, use provided mimeType or fall back to reasonable defaults\n const defaultMimeType = {\n image: 'image/jpeg',\n audio: 'audio/mp3',\n video: 'video/mp4',\n document: 'application/pdf',\n }[part.type]\n\n return {\n fileData: {\n fileUri: part.source.value,\n mimeType: part.source.mimeType ?? defaultMimeType,\n },\n }\n }\n }\n default: {\n const _exhaustiveCheck: never = part\n throw new Error(\n `Unsupported content part type: ${(_exhaustiveCheck as ContentPart).type}`,\n )\n }\n }\n }\n\n private formatMessages(\n messages: Array<ModelMessage>,\n ): GenerateContentParameters['contents'] {\n // Build a lookup from toolCallId → function name so functionResponse uses the\n // correct name instead of the raw call ID.\n const toolCallIdToName = new Map<string, string>()\n for (const msg of messages) {\n if (msg.role === 'assistant' && msg.toolCalls) {\n for (const tc of msg.toolCalls) {\n toolCallIdToName.set(tc.id, tc.function.name)\n }\n }\n }\n\n const formatted = messages.map((msg) => {\n const role: 'user' | 'model' = msg.role === 'assistant' ? 'model' : 'user'\n const parts: Array<Part> = []\n\n if (Array.isArray(msg.content)) {\n for (const contentPart of msg.content) {\n parts.push(this.convertContentPartToGemini(contentPart))\n }\n } else if (msg.content && msg.role !== 'tool') {\n parts.push({ text: msg.content })\n }\n\n if (msg.role === 'assistant' && msg.toolCalls?.length) {\n for (const toolCall of msg.toolCalls) {\n let parsedArgs: Record<string, unknown> = {}\n try {\n parsedArgs = toolCall.function.arguments\n ? (JSON.parse(toolCall.function.arguments) as Record<\n string,\n unknown\n >)\n : {}\n } catch {\n parsedArgs = toolCall.function.arguments as unknown as Record<\n string,\n unknown\n >\n }\n\n const thoughtSignature = (\n toolCall.metadata as GeminiToolCallMetadata | undefined\n )?.thoughtSignature\n // Gemini requires thoughtSignature at the Part level (sibling of\n // functionCall), not nested inside functionCall. Nesting it causes\n // the API to reject the next turn with\n // \"Function call is missing a thought_signature\".\n const part: Part = {\n functionCall: {\n id: toolCall.id,\n name: toolCall.function.name,\n args: parsedArgs,\n },\n }\n if (thoughtSignature) {\n part.thoughtSignature = thoughtSignature\n }\n parts.push(part)\n }\n }\n\n if (msg.role === 'tool' && msg.toolCallId) {\n const functionName =\n toolCallIdToName.get(msg.toolCallId) || msg.toolCallId\n parts.push({\n functionResponse: {\n id: msg.toolCallId,\n name: functionName,\n response: {\n content: msg.content || '',\n },\n } as any,\n })\n }\n\n return {\n role,\n parts: parts.length > 0 ? parts : [{ text: '' }],\n }\n })\n\n // Post-process: Gemini requires strictly alternating user/model roles.\n // Tool results are mapped to role:'user', which can create consecutive\n // user messages when followed by a new user message. Merge them.\n return this.mergeConsecutiveSameRoleMessages(formatted)\n }\n\n /**\n * Merge consecutive messages of the same role into a single message.\n * Gemini's API requires strictly alternating user/model roles.\n * Tool results are mapped to role:'user', which can collide with actual\n * user messages in multi-turn conversations.\n *\n * Also filters out empty model messages (e.g., from a previous failed request)\n * and deduplicates functionResponse parts with the same name (tool call ID).\n */\n private mergeConsecutiveSameRoleMessages(\n messages: Array<Content>,\n ): Array<Content> {\n const merged: Array<Content> = []\n\n for (const msg of messages) {\n const parts = msg.parts || []\n\n // Skip empty model messages (no parts or only empty text)\n if (msg.role === 'model') {\n const hasContent =\n parts.length > 0 &&\n !parts.every(\n (p) => 'text' in p && (p as { text: string }).text === '',\n )\n if (!hasContent) {\n continue\n }\n }\n\n const prev = merged[merged.length - 1]\n if (prev && prev.role === msg.role) {\n // Merge parts arrays\n prev.parts = [...(prev.parts || []), ...parts]\n } else {\n merged.push({ ...msg, parts: [...parts] })\n }\n }\n\n // Deduplicate functionResponse parts with the same name (tool call ID)\n for (const msg of merged) {\n if (!msg.parts) continue\n const seenFunctionResponseNames = new Set<string>()\n msg.parts = msg.parts.filter((part) => {\n if ('functionResponse' in part && part.functionResponse?.name) {\n if (seenFunctionResponseNames.has(part.functionResponse.name)) {\n return false\n }\n seenFunctionResponseNames.add(part.functionResponse.name)\n }\n return true\n })\n }\n\n return merged\n }\n\n private mapCommonOptionsToGemini(\n options: TextOptions<GeminiTextProviderOptions>,\n ) {\n const modelOpts = options.modelOptions\n const thinkingConfig = modelOpts?.thinkingConfig\n const requestOptions: GenerateContentParameters = {\n model: options.model,\n contents: this.formatMessages(options.messages),\n config: {\n ...modelOpts,\n temperature: options.temperature,\n topP: options.topP,\n maxOutputTokens: options.maxTokens,\n thinkingConfig: thinkingConfig\n ? {\n ...thinkingConfig,\n thinkingLevel: thinkingConfig.thinkingLevel\n ? // Enum is provided by the SDK, we use it for the type but cast it to string constants, here we just cast them back\n (thinkingConfig.thinkingLevel as ThinkingLevel)\n : undefined,\n }\n : undefined,\n systemInstruction: options.systemPrompts?.join('\\n'),\n tools: convertToolsToProviderFormat(options.tools),\n },\n }\n\n return requestOptions\n }\n}\n\n/**\n * Creates a Gemini text adapter with explicit API key.\n * Type resolution happens here at the call site.\n */\nexport function createGeminiChat<TModel extends (typeof GEMINI_MODELS)[number]>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiTextConfig, 'apiKey'>,\n): GeminiTextAdapter<\n TModel,\n ResolveProviderOptions<TModel>,\n ResolveInputModalities<TModel>,\n ResolveToolCapabilities<TModel>\n> {\n return new GeminiTextAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Gemini text adapter with automatic API key detection.\n * Type resolution happens here at the call site.\n */\nexport function geminiText<TModel extends (typeof GEMINI_MODELS)[number]>(\n model: TModel,\n config?: Omit<GeminiTextConfig, 'apiKey'>,\n): GeminiTextAdapter<\n TModel,\n ResolveProviderOptions<TModel>,\n ResolveInputModalities<TModel>,\n ResolveToolCapabilities<TModel>\n> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiChat(model, apiKey, config)\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -89,3 +89,16 @@ export interface GeminiMessageMetadataByModality {
|
|
|
89
89
|
video: GeminiVideoMetadata;
|
|
90
90
|
document: GeminiDocumentMetadata;
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Provider-specific metadata that round-trips with each Gemini tool call.
|
|
94
|
+
*
|
|
95
|
+
* `thoughtSignature` is emitted by Gemini 3.x (and 2.5 thinking) models on
|
|
96
|
+
* the Part containing the `functionCall`. The same signature must be echoed
|
|
97
|
+
* back at the Part level on the next turn or the API rejects the request
|
|
98
|
+
* with `400 INVALID_ARGUMENT: "Function call is missing a thought_signature"`.
|
|
99
|
+
*
|
|
100
|
+
* @see https://ai.google.dev/gemini-api/docs/thinking
|
|
101
|
+
*/
|
|
102
|
+
export interface GeminiToolCallMetadata {
|
|
103
|
+
thoughtSignature?: string;
|
|
104
|
+
}
|
package/dist/esm/utils/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GoogleGenAI } from "@google/genai";
|
|
2
|
+
import { generateId as generateId$1, getApiKeyFromEnv } from "@tanstack/ai-utils";
|
|
2
3
|
function createGeminiClient(config) {
|
|
3
4
|
return new GoogleGenAI({
|
|
4
5
|
...config,
|
|
@@ -6,17 +7,20 @@ function createGeminiClient(config) {
|
|
|
6
7
|
});
|
|
7
8
|
}
|
|
8
9
|
function getGeminiApiKeyFromEnv() {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
10
|
+
try {
|
|
11
|
+
return getApiKeyFromEnv("GOOGLE_API_KEY");
|
|
12
|
+
} catch {
|
|
13
|
+
try {
|
|
14
|
+
return getApiKeyFromEnv("GEMINI_API_KEY");
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"GOOGLE_API_KEY or GEMINI_API_KEY is not set. Please set one of these environment variables or pass the API key directly."
|
|
18
|
+
);
|
|
19
|
+
}
|
|
15
20
|
}
|
|
16
|
-
return key;
|
|
17
21
|
}
|
|
18
22
|
function generateId(prefix) {
|
|
19
|
-
return
|
|
23
|
+
return generateId$1(prefix);
|
|
20
24
|
}
|
|
21
25
|
export {
|
|
22
26
|
createGeminiClient,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sources":["../../../src/utils/client.ts"],"sourcesContent":["import { GoogleGenAI } from '@google/genai'\nimport type { GoogleGenAIOptions } from '@google/genai'\n\nexport interface GeminiClientConfig extends GoogleGenAIOptions {\n apiKey: string\n}\n\n/**\n * Creates a Google Generative AI client instance\n */\nexport function createGeminiClient(config: GeminiClientConfig): GoogleGenAI {\n return new GoogleGenAI({\n ...config,\n apiKey: config.apiKey,\n })\n}\n\n/**\n * Gets Google API key from environment variables\n * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found\n */\nexport function getGeminiApiKeyFromEnv(): string {\n
|
|
1
|
+
{"version":3,"file":"client.js","sources":["../../../src/utils/client.ts"],"sourcesContent":["import { GoogleGenAI } from '@google/genai'\nimport { generateId as _generateId, getApiKeyFromEnv } from '@tanstack/ai-utils'\nimport type { GoogleGenAIOptions } from '@google/genai'\n\nexport interface GeminiClientConfig extends GoogleGenAIOptions {\n apiKey: string\n}\n\n/**\n * Creates a Google Generative AI client instance\n */\nexport function createGeminiClient(config: GeminiClientConfig): GoogleGenAI {\n return new GoogleGenAI({\n ...config,\n apiKey: config.apiKey,\n })\n}\n\n/**\n * Gets Google API key from environment variables\n * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found\n */\nexport function getGeminiApiKeyFromEnv(): string {\n try {\n return getApiKeyFromEnv('GOOGLE_API_KEY')\n } catch {\n try {\n return getApiKeyFromEnv('GEMINI_API_KEY')\n } catch {\n throw new Error(\n 'GOOGLE_API_KEY or GEMINI_API_KEY is not set. Please set one of these environment variables or pass the API key directly.',\n )\n }\n }\n}\n\n/**\n * Generates a unique ID with a prefix\n */\nexport function generateId(prefix: string): string {\n return _generateId(prefix)\n}\n"],"names":["_generateId"],"mappings":";;AAWO,SAAS,mBAAmB,QAAyC;AAC1E,SAAO,IAAI,YAAY;AAAA,IACrB,GAAG;AAAA,IACH,QAAQ,OAAO;AAAA,EAAA,CAChB;AACH;AAMO,SAAS,yBAAiC;AAC/C,MAAI;AACF,WAAO,iBAAiB,gBAAgB;AAAA,EAC1C,QAAQ;AACN,QAAI;AACF,aAAO,iBAAiB,gBAAgB;AAAA,IAC1C,QAAQ;AACN,YAAM,IAAI;AAAA,QACR;AAAA,MAAA;AAAA,IAEJ;AAAA,EACF;AACF;AAKO,SAAS,WAAW,QAAwB;AACjD,SAAOA,aAAY,MAAM;AAC3B;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-gemini",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Google Gemini adapter for TanStack AI",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,16 +34,17 @@
|
|
|
34
34
|
"adapter"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@google/genai": "^1.43.0"
|
|
37
|
+
"@google/genai": "^1.43.0",
|
|
38
|
+
"@tanstack/ai-utils": "0.2.0"
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
|
40
|
-
"@tanstack/ai": "^0.
|
|
41
|
+
"@tanstack/ai": "^0.16.0"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@vitest/coverage-v8": "4.0.14",
|
|
44
45
|
"vite": "^7.2.7",
|
|
45
46
|
"zod": "^4.2.0",
|
|
46
|
-
"@tanstack/ai": "0.
|
|
47
|
+
"@tanstack/ai": "0.16.0"
|
|
47
48
|
},
|
|
48
49
|
"scripts": {
|
|
49
50
|
"build": "vite build",
|
package/src/adapters/text.ts
CHANGED
|
@@ -33,7 +33,10 @@ import type {
|
|
|
33
33
|
TextOptions,
|
|
34
34
|
} from '@tanstack/ai'
|
|
35
35
|
import type { ExternalTextProviderOptions } from '../text/text-provider-options'
|
|
36
|
-
import type {
|
|
36
|
+
import type {
|
|
37
|
+
GeminiMessageMetadataByModality,
|
|
38
|
+
GeminiToolCallMetadata,
|
|
39
|
+
} from '../message-types'
|
|
37
40
|
import type { GeminiClientConfig } from '../utils'
|
|
38
41
|
|
|
39
42
|
/** Cast an event object to StreamChunk. Adapters construct events with string
|
|
@@ -104,7 +107,8 @@ export class GeminiTextAdapter<
|
|
|
104
107
|
TProviderOptions,
|
|
105
108
|
TInputModalities,
|
|
106
109
|
GeminiMessageMetadataByModality,
|
|
107
|
-
TToolCapabilities
|
|
110
|
+
TToolCapabilities,
|
|
111
|
+
GeminiToolCallMetadata
|
|
108
112
|
> {
|
|
109
113
|
readonly kind = 'text' as const
|
|
110
114
|
readonly name = 'gemini' as const
|
|
@@ -385,6 +389,11 @@ export class GeminiTextAdapter<
|
|
|
385
389
|
`${functionCall.name}_${Date.now()}_${nextToolIndex}`
|
|
386
390
|
const functionArgs = functionCall.args || {}
|
|
387
391
|
|
|
392
|
+
// Gemini emits thoughtSignature as a Part-level sibling of
|
|
393
|
+
// functionCall (per @google/genai Part type), not nested inside
|
|
394
|
+
// functionCall itself.
|
|
395
|
+
const partThoughtSignature = part.thoughtSignature || undefined
|
|
396
|
+
|
|
388
397
|
let toolCallData = toolCallMap.get(toolCallId)
|
|
389
398
|
if (!toolCallData) {
|
|
390
399
|
toolCallData = {
|
|
@@ -395,11 +404,13 @@ export class GeminiTextAdapter<
|
|
|
395
404
|
: JSON.stringify(functionArgs),
|
|
396
405
|
index: nextToolIndex++,
|
|
397
406
|
started: false,
|
|
398
|
-
thoughtSignature:
|
|
399
|
-
(functionCall as any).thoughtSignature || undefined,
|
|
407
|
+
thoughtSignature: partThoughtSignature,
|
|
400
408
|
}
|
|
401
409
|
toolCallMap.set(toolCallId, toolCallData)
|
|
402
410
|
} else {
|
|
411
|
+
if (!toolCallData.thoughtSignature && partThoughtSignature) {
|
|
412
|
+
toolCallData.thoughtSignature = partThoughtSignature
|
|
413
|
+
}
|
|
403
414
|
try {
|
|
404
415
|
const existingArgs = JSON.parse(toolCallData.args)
|
|
405
416
|
const newArgs =
|
|
@@ -428,9 +439,9 @@ export class GeminiTextAdapter<
|
|
|
428
439
|
timestamp,
|
|
429
440
|
index: toolCallData.index,
|
|
430
441
|
...(toolCallData.thoughtSignature && {
|
|
431
|
-
|
|
442
|
+
metadata: {
|
|
432
443
|
thoughtSignature: toolCallData.thoughtSignature,
|
|
433
|
-
},
|
|
444
|
+
} satisfies GeminiToolCallMetadata,
|
|
434
445
|
}),
|
|
435
446
|
})
|
|
436
447
|
}
|
|
@@ -707,16 +718,24 @@ export class GeminiTextAdapter<
|
|
|
707
718
|
>
|
|
708
719
|
}
|
|
709
720
|
|
|
710
|
-
const thoughtSignature =
|
|
711
|
-
|
|
712
|
-
|
|
721
|
+
const thoughtSignature = (
|
|
722
|
+
toolCall.metadata as GeminiToolCallMetadata | undefined
|
|
723
|
+
)?.thoughtSignature
|
|
724
|
+
// Gemini requires thoughtSignature at the Part level (sibling of
|
|
725
|
+
// functionCall), not nested inside functionCall. Nesting it causes
|
|
726
|
+
// the API to reject the next turn with
|
|
727
|
+
// "Function call is missing a thought_signature".
|
|
728
|
+
const part: Part = {
|
|
713
729
|
functionCall: {
|
|
714
730
|
id: toolCall.id,
|
|
715
731
|
name: toolCall.function.name,
|
|
716
732
|
args: parsedArgs,
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
733
|
+
},
|
|
734
|
+
}
|
|
735
|
+
if (thoughtSignature) {
|
|
736
|
+
part.thoughtSignature = thoughtSignature
|
|
737
|
+
}
|
|
738
|
+
parts.push(part)
|
|
720
739
|
}
|
|
721
740
|
}
|
|
722
741
|
|
package/src/message-types.ts
CHANGED
|
@@ -130,3 +130,17 @@ export interface GeminiMessageMetadataByModality {
|
|
|
130
130
|
video: GeminiVideoMetadata
|
|
131
131
|
document: GeminiDocumentMetadata
|
|
132
132
|
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Provider-specific metadata that round-trips with each Gemini tool call.
|
|
136
|
+
*
|
|
137
|
+
* `thoughtSignature` is emitted by Gemini 3.x (and 2.5 thinking) models on
|
|
138
|
+
* the Part containing the `functionCall`. The same signature must be echoed
|
|
139
|
+
* back at the Part level on the next turn or the API rejects the request
|
|
140
|
+
* with `400 INVALID_ARGUMENT: "Function call is missing a thought_signature"`.
|
|
141
|
+
*
|
|
142
|
+
* @see https://ai.google.dev/gemini-api/docs/thinking
|
|
143
|
+
*/
|
|
144
|
+
export interface GeminiToolCallMetadata {
|
|
145
|
+
thoughtSignature?: string
|
|
146
|
+
}
|
package/src/utils/client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GoogleGenAI } from '@google/genai'
|
|
2
|
+
import { generateId as _generateId, getApiKeyFromEnv } from '@tanstack/ai-utils'
|
|
2
3
|
import type { GoogleGenAIOptions } from '@google/genai'
|
|
3
4
|
|
|
4
5
|
export interface GeminiClientConfig extends GoogleGenAIOptions {
|
|
@@ -20,26 +21,22 @@ export function createGeminiClient(config: GeminiClientConfig): GoogleGenAI {
|
|
|
20
21
|
* @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found
|
|
21
22
|
*/
|
|
22
23
|
export function getGeminiApiKeyFromEnv(): string {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
'GOOGLE_API_KEY or GEMINI_API_KEY is required. Please set it in your environment variables or use the factory function with an explicit API key.',
|
|
34
|
-
)
|
|
24
|
+
try {
|
|
25
|
+
return getApiKeyFromEnv('GOOGLE_API_KEY')
|
|
26
|
+
} catch {
|
|
27
|
+
try {
|
|
28
|
+
return getApiKeyFromEnv('GEMINI_API_KEY')
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error(
|
|
31
|
+
'GOOGLE_API_KEY or GEMINI_API_KEY is not set. Please set one of these environment variables or pass the API key directly.',
|
|
32
|
+
)
|
|
33
|
+
}
|
|
35
34
|
}
|
|
36
|
-
|
|
37
|
-
return key
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
/**
|
|
41
38
|
* Generates a unique ID with a prefix
|
|
42
39
|
*/
|
|
43
40
|
export function generateId(prefix: string): string {
|
|
44
|
-
return
|
|
41
|
+
return _generateId(prefix)
|
|
45
42
|
}
|