@tanstack/ai-gemini 0.30.0 → 0.31.2
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 +1 -1
- package/dist/esm/adapters/text.js +5 -5
- package/dist/esm/adapters/text.js.map +1 -1
- package/dist/esm/adapters/tts.d.ts +5 -1
- package/dist/esm/adapters/tts.js +18 -4
- package/dist/esm/adapters/tts.js.map +1 -1
- package/package.json +4 -4
- package/src/adapters/text.ts +10 -6
- package/src/adapters/tts.ts +38 -13
|
@@ -77,7 +77,7 @@ export declare class GeminiTextAdapter<TModel extends (typeof GEMINI_MODELS)[num
|
|
|
77
77
|
* user messages in multi-turn conversations.
|
|
78
78
|
*
|
|
79
79
|
* Also filters out empty model messages (e.g., from a previous failed request)
|
|
80
|
-
* and deduplicates functionResponse parts with the same
|
|
80
|
+
* and deduplicates functionResponse parts with the same id (tool call ID).
|
|
81
81
|
*/
|
|
82
82
|
private mergeConsecutiveSameRoleMessages;
|
|
83
83
|
private mapCommonOptionsToGemini;
|
|
@@ -683,7 +683,7 @@ var GeminiTextAdapter = class extends BaseTextAdapter {
|
|
|
683
683
|
* user messages in multi-turn conversations.
|
|
684
684
|
*
|
|
685
685
|
* Also filters out empty model messages (e.g., from a previous failed request)
|
|
686
|
-
* and deduplicates functionResponse parts with the same
|
|
686
|
+
* and deduplicates functionResponse parts with the same id (tool call ID).
|
|
687
687
|
*/
|
|
688
688
|
mergeConsecutiveSameRoleMessages(messages) {
|
|
689
689
|
const merged = [];
|
|
@@ -701,11 +701,11 @@ var GeminiTextAdapter = class extends BaseTextAdapter {
|
|
|
701
701
|
}
|
|
702
702
|
for (const msg of merged) {
|
|
703
703
|
if (!msg.parts) continue;
|
|
704
|
-
const
|
|
704
|
+
const seenFunctionResponseIds = /* @__PURE__ */ new Set();
|
|
705
705
|
msg.parts = msg.parts.filter((part) => {
|
|
706
|
-
if ("functionResponse" in part && part.functionResponse?.
|
|
707
|
-
if (
|
|
708
|
-
|
|
706
|
+
if ("functionResponse" in part && part.functionResponse?.id) {
|
|
707
|
+
if (seenFunctionResponseIds.has(part.functionResponse.id)) return false;
|
|
708
|
+
seenFunctionResponseIds.add(part.functionResponse.id);
|
|
709
709
|
}
|
|
710
710
|
return true;
|
|
711
711
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.js","names":[],"sources":["../../../src/adapters/text.ts"],"sourcesContent":["import { FinishReason } from '@google/genai'\nimport { EventType, normalizeSystemPrompts } from '@tanstack/ai'\nimport { toRunErrorRawEvent } from '@tanstack/ai/adapter-internals'\nimport { BaseTextAdapter } from '@tanstack/ai/adapters'\nimport { convertToolsToProviderFormat } from '../tools/tool-converter'\nimport { buildGeminiUsage } from '../usage'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport { GEMINI_COMBINED_TOOLS_AND_SCHEMA_MODELS } from '../model-meta'\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 VideoMetadata,\n} from '@google/genai'\nimport type {\n ContentPart,\n Modality,\n ModelMessage,\n AdapterYieldChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type { ExternalTextProviderOptions } from '../text/text-provider-options'\nimport type {\n GeminiMessageMetadataByModality,\n GeminiToolCallMetadata,\n GeminiVideoMetadata,\n GeminiVideoProcessing,\n} from '../message-types'\nimport type { GeminiClientConfig } from '../utils/client'\n\n/**\n * Fallback MIME types for URL-sourced media parts that don't specify one.\n */\nconst DEFAULT_MEDIA_MIME_TYPES = {\n image: 'image/jpeg',\n audio: 'audio/mp3',\n video: 'video/mp4',\n document: 'application/pdf',\n} as const\n\n/**\n * Content block shape for an Interactions API `input` step. The installed\n * @google/genai types predate the video `processing` field, so we model the\n * subset we emit and cast at the call site.\n */\ntype InteractionContent =\n | { type: 'text'; text: string }\n | {\n type: 'video'\n uri?: string\n data?: string\n mime_type?: string\n processing?: GeminiVideoProcessing\n }\n | {\n type: 'image' | 'audio' | 'document'\n uri?: string\n data?: string\n mime_type?: string\n }\n\ninterface InteractionStep {\n type: 'user_input' | 'model_output'\n content: Array<InteractionContent>\n}\n\n/** True when any message carries a video part requesting agentic processing. */\nfunction hasAgenticVideo(messages: Array<ModelMessage>): boolean {\n return messages.some(\n (msg) =>\n Array.isArray(msg.content) &&\n msg.content.some(\n (part) =>\n part.type === 'video' &&\n (part.metadata as GeminiVideoMetadata | undefined)?.processing ===\n 'agentic',\n ),\n )\n}\n\n/** Convert a single content part to an Interactions API content block. */\nfunction contentPartToInteraction(part: ContentPart): InteractionContent {\n if (part.type === 'text') {\n return { type: 'text', text: part.content }\n }\n\n const source = part.source\n const mimeType =\n source.type === 'data'\n ? source.mimeType\n : (source.mimeType ?? DEFAULT_MEDIA_MIME_TYPES[part.type])\n const base =\n source.type === 'data'\n ? { data: source.value, mime_type: mimeType }\n : { uri: source.value, mime_type: mimeType }\n\n if (part.type === 'video') {\n const processing = (part.metadata as GeminiVideoMetadata | undefined)\n ?.processing\n return { type: 'video', ...base, ...(processing && { processing }) }\n }\n return { type: part.type, ...base }\n}\n\n/**\n * Build the Interactions API `input` from chat messages. Each user/assistant\n * message becomes a `user_input` / `model_output` step wrapping its content\n * blocks — the wrapping the Python SDK performs implicitly but the JS SDK\n * does not. Tool messages are skipped (unsupported on this path).\n */\nfunction buildInteractionsInput(\n messages: Array<ModelMessage>,\n): Array<InteractionStep> {\n const steps: Array<InteractionStep> = []\n for (const msg of messages) {\n if (msg.role === 'tool') continue\n const stepType = msg.role === 'assistant' ? 'model_output' : 'user_input'\n const content: Array<InteractionContent> = []\n if (Array.isArray(msg.content)) {\n for (const part of msg.content) {\n content.push(contentPartToInteraction(part))\n }\n } else if (msg.content) {\n content.push({ type: 'text', text: msg.content })\n }\n if (content.length > 0) {\n steps.push({ type: stepType, content })\n }\n }\n return steps\n}\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 override readonly kind = 'text' as const\n readonly name = 'gemini' as const\n\n private readonly 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<AdapterYieldChunk> {\n // Agentic video understanding is only exposed through the Interactions API,\n // not generateContent. Detect it and take that path instead.\n if (hasAgenticVideo(options.messages)) {\n yield* this.interactionsStream(options)\n return\n }\n\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 rawEvent = toRunErrorRawEvent(error)\n logger.errors('gemini.chatStream fatal', {\n error,\n source: 'gemini.chatStream',\n })\n yield {\n type: EventType.RUN_ERROR,\n model: options.model,\n timestamp: Date.now(),\n message:\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during the chat stream.',\n // Forward the provider's structured error body when present (see\n // toRunErrorRawEvent); omitted otherwise.\n ...(rawEvent !== undefined && { rawEvent }),\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 * Agentic video-understanding path via the Interactions API.\n *\n * The Interactions API (unlike `generateContent`) requires message parts to\n * be wrapped in `user_input` / `model_output` steps, and it accepts the\n * `processing: 'agentic'` video flag. This is a non-streaming call whose\n * single text result is re-emitted as AG-UI stream chunks.\n */\n private async *interactionsStream(\n options: TextOptions<GeminiTextProviderOptions>,\n ): AsyncIterable<AdapterYieldChunk> {\n const model = options.model\n const { logger } = options\n const runId = options.runId ?? generateId(this.name)\n const threadId = options.threadId ?? generateId(this.name)\n const messageId = generateId(this.name)\n\n try {\n logger.request(\n `activity=chat provider=gemini model=${model} messages=${options.messages.length} mode=interactions-agentic-video`,\n { provider: 'gemini', model },\n )\n\n const normalizedPrompts = normalizeSystemPrompts(options.systemPrompts)\n const systemInstruction =\n normalizedPrompts.length > 0\n ? normalizedPrompts.map((p) => p.content).join('\\n')\n : undefined\n\n const input = buildInteractionsInput(options.messages)\n\n // The installed @google/genai (2.10.0) Interactions `VideoContent` type\n // predates the `processing` field, so the structurally-built input is\n // cast at the call boundary. The SDK forwards it to the wire unchanged.\n const interaction = await this.client.interactions.create({\n model,\n ...(systemInstruction !== undefined && {\n system_instruction: systemInstruction,\n }),\n input: input as never,\n })\n\n const text = interaction.output_text ?? ''\n\n yield {\n type: EventType.RUN_STARTED,\n runId,\n threadId,\n model,\n timestamp: Date.now(),\n parentRunId: options.parentRunId,\n }\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId,\n model,\n timestamp: Date.now(),\n role: 'assistant',\n }\n if (text) {\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId,\n model,\n timestamp: Date.now(),\n delta: text,\n content: text,\n }\n }\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId,\n model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: Date.now(),\n finishReason: 'stop',\n }\n } catch (error) {\n const rawEvent = toRunErrorRawEvent(error)\n logger.errors('gemini.interactionsStream fatal', {\n error,\n source: 'gemini.interactionsStream',\n })\n yield {\n type: EventType.RUN_ERROR,\n model,\n timestamp: Date.now(),\n message:\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during the chat stream.',\n ...(rawEvent !== undefined && { rawEvent }),\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(jsonContentParseError(rawText, 'structured output'))\n }\n\n return {\n data: parsed,\n rawText,\n usage: result.usageMetadata\n ? buildGeminiUsage(result.usageMetadata)\n : undefined,\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 * Stream schema-constrained JSON from Gemini natively.\n *\n * `chat({ outputSchema, stream: true })` calls this when the adapter\n * implements it. Without it, the engine buffers `structuredOutput()` and\n * emits one synthetic delta.\n */\n async *structuredOutputStream(\n options: StructuredOutputOptions<GeminiTextProviderOptions>,\n ): AsyncIterable<AdapterYieldChunk> {\n const { chatOptions: requestedChatOptions, outputSchema } = options\n const chatOptions = {\n ...requestedChatOptions,\n runId: requestedChatOptions.runId ?? generateId(this.name),\n }\n const mappedOptions = this.mapCommonOptionsToGemini(chatOptions)\n\n try {\n chatOptions.logger.request(\n `activity=structuredOutputStream provider=gemini model=${this.model} messages=${chatOptions.messages.length}`,\n { provider: 'gemini', model: this.model },\n )\n const result = await this.client.models.generateContentStream({\n ...mappedOptions,\n config: {\n ...mappedOptions.config,\n responseMimeType: 'application/json',\n responseSchema: outputSchema,\n },\n })\n\n let rawText = ''\n let finished:\n | Extract<AdapterYieldChunk, { type: typeof EventType.RUN_FINISHED }>\n | undefined\n let failed = false\n for await (const chunk of this.processStreamChunks(\n result,\n chatOptions,\n chatOptions.logger,\n )) {\n if (chunk.type === EventType.TEXT_MESSAGE_CONTENT) {\n rawText += chunk.delta\n }\n if (chunk.type === EventType.RUN_ERROR) failed = true\n if (chunk.type === EventType.RUN_FINISHED) {\n finished = chunk\n } else {\n yield chunk\n }\n }\n\n if (failed) return\n if (!finished) {\n yield structuredStreamError(\n chatOptions,\n 'Gemini structured-output stream ended without a terminal event',\n 'truncated-stream',\n )\n return\n }\n if (!rawText) {\n yield structuredStreamError(\n chatOptions,\n 'Gemini structured-output stream contained no content',\n 'empty-response',\n )\n return\n }\n\n let object: unknown\n try {\n object = JSON.parse(rawText)\n } catch {\n yield structuredStreamError(\n chatOptions,\n jsonContentParseError(rawText, 'Gemini structured-output stream'),\n 'parse-error',\n )\n return\n }\n\n yield {\n type: EventType.CUSTOM,\n name: 'structured-output.complete',\n value: { object, raw: rawText },\n model: chatOptions.model,\n timestamp: Date.now(),\n }\n yield { ...finished, timestamp: Date.now() }\n } catch (error) {\n const rawEvent = toRunErrorRawEvent(error)\n const message =\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during structured output streaming.'\n chatOptions.logger.errors('gemini.structuredOutputStream fatal', {\n error,\n source: 'gemini.structuredOutputStream',\n })\n yield {\n ...structuredStreamError(chatOptions, message, 'provider-error'),\n ...(rawEvent !== undefined && { rawEvent }),\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<AdapterYieldChunk> {\n const model = options.model\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 {\n type: EventType.RUN_STARTED,\n runId,\n threadId,\n model,\n timestamp: Date.now(),\n parentRunId: options.parentRunId,\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 {\n type: EventType.REASONING_START,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: reasoningMessageId,\n role: 'reasoning' as const,\n model,\n timestamp: Date.now(),\n }\n\n // Legacy STEP events (kept during transition)\n yield {\n type: EventType.STEP_STARTED,\n stepName: stepId,\n stepId,\n model,\n timestamp: Date.now(),\n stepType: 'thinking',\n }\n }\n\n accumulatedThinking += part.text\n\n // Spec REASONING content event — reasoningMessageId is set in the\n // hasEmittedStepStarted block above (entered on the same `part.thought` path)\n if (!reasoningMessageId) continue\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: reasoningMessageId,\n delta: part.text,\n model,\n timestamp: Date.now(),\n }\n\n // Legacy STEP event\n yield {\n type: EventType.STEP_FINISHED,\n stepName: stepId || generateId(this.name),\n stepId: stepId || generateId(this.name),\n model,\n timestamp: Date.now(),\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 {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\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 {\n type: EventType.TEXT_MESSAGE_START,\n messageId,\n model,\n timestamp: Date.now(),\n role: 'assistant',\n }\n }\n\n accumulatedContent += part.text\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId,\n model,\n timestamp: Date.now(),\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 // Only set thoughtSignature when present — under EOPT, the\n // optional field cannot accept an explicit `undefined`.\n ...(partThoughtSignature !== undefined && {\n thoughtSignature: partThoughtSignature,\n }),\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 {\n type: EventType.TOOL_CALL_START,\n toolCallId,\n toolCallName: toolCallData.name,\n toolName: toolCallData.name,\n parentMessageId: messageId,\n model,\n timestamp: Date.now(),\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 {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId,\n model,\n timestamp: Date.now(),\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 {\n type: EventType.TEXT_MESSAGE_START,\n messageId,\n model,\n timestamp: Date.now(),\n role: 'assistant',\n }\n }\n\n accumulatedContent += chunk.data\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId,\n model,\n timestamp: Date.now(),\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 // Emit TOOL_CALL_END for all tracked tool calls. functionCall parts on\n // this chunk (including UNEXPECTED_TOOL_CALL finishes) were already\n // registered and started by the per-part loop above.\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 {\n type: EventType.TOOL_CALL_END,\n toolCallId,\n toolCallName: toolCallData.name,\n toolName: toolCallData.name,\n model,\n timestamp: Date.now(),\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 {\n type: EventType.RUN_ERROR,\n runId,\n model,\n timestamp: Date.now(),\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 {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\n }\n }\n\n // Emit TEXT_MESSAGE_END if we had text content\n if (hasEmittedTextMessageStart) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId,\n model,\n timestamp: Date.now(),\n }\n }\n\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: Date.now(),\n finishReason: toolCallMap.size > 0 ? 'tool_calls' : 'stop',\n // RunFinishedEvent.usage is `usage?: {...}` (no `| undefined`) under\n // exactOptionalPropertyTypes; only include it when usageMetadata is\n // present rather than assigning an explicit `undefined`.\n ...(chunk.usageMetadata && {\n usage: buildGeminiUsage(chunk.usageMetadata),\n }),\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 const geminiPart: Part =\n part.source.type === 'data'\n ? {\n inlineData: {\n data: part.source.value,\n mimeType: part.source.mimeType,\n },\n }\n : {\n fileData: {\n fileUri: part.source.value,\n // For URL sources, use provided mimeType or fall back to\n // reasonable defaults.\n mimeType:\n part.source.mimeType ?? DEFAULT_MEDIA_MIME_TYPES[part.type],\n },\n }\n\n // Apply single-pass video sampling controls (fps / clip offsets) from\n // the part metadata. `processing: 'agentic'` is handled separately via\n // the Interactions API and never reaches this generateContent path.\n if (part.type === 'video') {\n const meta = part.metadata as GeminiVideoMetadata | undefined\n const videoMetadata: VideoMetadata = {\n ...(meta?.fps !== undefined && { fps: meta.fps }),\n ...(meta?.startOffset !== undefined && {\n startOffset: meta.startOffset,\n }),\n ...(meta?.endOffset !== undefined && { endOffset: meta.endOffset }),\n }\n if (Object.keys(videoMetadata).length > 0) {\n geminiPart.videoMetadata = videoMetadata\n }\n }\n\n return geminiPart\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 = {}\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 const toolContent = msg.content\n if (Array.isArray(toolContent)) {\n const textChunks: Array<string> = []\n const mediaParts: Array<Part> = []\n for (const part of toolContent) {\n if (part.type === 'text') {\n textChunks.push(part.content)\n } else if (part.source.type === 'data') {\n mediaParts.push({\n inlineData: {\n data: part.source.value,\n mimeType: part.source.mimeType,\n },\n })\n } else {\n const defaultMimeType = {\n image: 'image/jpeg',\n audio: 'audio/mp3',\n video: 'video/mp4',\n document: 'application/pdf',\n }[part.type]\n mediaParts.push({\n fileData: {\n fileUri: part.source.value,\n mimeType: part.source.mimeType ?? defaultMimeType,\n },\n })\n }\n }\n parts.push({\n functionResponse: {\n id: msg.toolCallId,\n name: functionName,\n response: { content: textChunks.join('\\n') },\n ...(mediaParts.length > 0 && { parts: mediaParts }),\n },\n })\n } else {\n parts.push({\n functionResponse: {\n id: msg.toolCallId,\n name: functionName,\n response: { content: toolContent || '' },\n },\n })\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 // Separate `thinkingConfig` from the other model options so the loose\n // local `thinkingLevel?: keyof typeof ThinkingLevel` type doesn't leak\n // into the SDK config object via the `...modelOpts` spread — we re-add a\n // properly-typed `ThinkingConfig` below.\n const { thinkingConfig, ...modelOpts } = options.modelOptions ?? {}\n // Build the thinkingConfig payload only when the caller actually supplied\n // one. Our local `thinkingLevel` is typed as `keyof typeof ThinkingLevel`\n // (string union) so users can pass plain strings; the SDK target is the\n // `ThinkingLevel` enum, and every field is `field?: T` under EOPT — so we\n // re-emit fields via conditional spreads.\n const mappedThinkingConfig = thinkingConfig\n ? {\n ...(thinkingConfig.includeThoughts !== undefined && {\n includeThoughts: thinkingConfig.includeThoughts,\n }),\n ...(thinkingConfig.thinkingBudget !== undefined && {\n thinkingBudget: thinkingConfig.thinkingBudget,\n }),\n ...(thinkingConfig.thinkingLevel\n ? {\n thinkingLevel: thinkingConfig.thinkingLevel as ThinkingLevel,\n }\n : {}),\n }\n : undefined\n\n const normalizedPrompts = normalizeSystemPrompts(options.systemPrompts)\n const systemInstruction =\n normalizedPrompts.length > 0\n ? normalizedPrompts.map((p) => p.content).join('\\n')\n : undefined\n\n // Native combined mode (issue #605): when the engine threads\n // `outputSchema` through TextOptions, the adapter declared\n // `supportsCombinedToolsAndSchema` (Gemini 3.x only). The schema is\n // already JSON Schema (pre-converted at the activity boundary). Wire\n // it into `config.responseSchema` + `responseMimeType: 'application/json'`\n // alongside any `tools` — the model emits function calls during the\n // agent loop and the schema-constrained JSON on its natural final\n // turn, so the engine can harvest it without the separate\n // `structuredOutput` finalization round-trip.\n const combinedSchema = options.outputSchema as\n | Record<string, unknown>\n | undefined\n const combinedSchemaConfig = combinedSchema\n ? {\n responseMimeType: 'application/json' as const,\n responseSchema: combinedSchema,\n }\n : undefined\n\n // Vendor `GenerateContentConfig` fields are `field?: T` (no `| undefined`)\n // under EOPT, so spread each common option only when present rather than\n // emitting `field: undefined`s into the wire payload.\n const requestOptions: GenerateContentParameters = {\n model: options.model,\n contents: this.formatMessages(options.messages),\n config: {\n ...modelOpts,\n ...(mappedThinkingConfig !== undefined && {\n thinkingConfig: mappedThinkingConfig,\n }),\n ...(systemInstruction !== undefined && { systemInstruction }),\n tools: convertToolsToProviderFormat(options.tools),\n ...(combinedSchemaConfig ?? {}),\n // Forward the caller's abort signal so cancellation reaches the SDK\n // request, matching the OpenAI-compatible adapters (issue #1374).\n ...(options.request?.signal != null && {\n abortSignal: options.request.signal,\n }),\n },\n }\n\n return requestOptions\n }\n\n /**\n * Gemini 3.x natively combines `tools` + `responseSchema` in a single\n * streaming `generateContentStream` call (issue #605). Gemini 2.x is\n * documented as brittle for the combination and keeps the engine's\n * legacy finalization path.\n */\n supportsCombinedToolsAndSchema(): boolean {\n return GEMINI_COMBINED_TOOLS_AND_SCHEMA_MODELS.has(this.model)\n }\n}\n\nfunction jsonContentParseError(rawText: string, label: string) {\n const snippet = rawText.slice(0, 200)\n const ellipsis = rawText.length > 200 ? '...' : ''\n return `Failed to parse ${label} as JSON. Content: ${snippet}${ellipsis}`\n}\n\nfunction structuredStreamError(\n options: TextOptions<GeminiTextProviderOptions>,\n message: string,\n code: string,\n): Extract<AdapterYieldChunk, { type: typeof EventType.RUN_ERROR }> {\n return {\n type: EventType.RUN_ERROR,\n runId: options.runId,\n model: options.model,\n timestamp: Date.now(),\n message,\n code,\n error: { message, code },\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"],"mappings":";;;;;;;;;;;;;AAmDA,IAAM,2BAA2B;CAC/B,OAAO;CACP,OAAO;CACP,OAAO;CACP,UAAU;AACZ;;AA6BA,SAAS,gBAAgB,UAAwC;CAC/D,OAAO,SAAS,MACb,QACC,MAAM,QAAQ,IAAI,OAAO,KACzB,IAAI,QAAQ,MACT,SACC,KAAK,SAAS,WACb,KAAK,UAA8C,eAClD,SACN,CACJ;AACF;;AAGA,SAAS,yBAAyB,MAAuC;CACvE,IAAI,KAAK,SAAS,QAChB,OAAO;EAAE,MAAM;EAAQ,MAAM,KAAK;CAAQ;CAG5C,MAAM,SAAS,KAAK;CACpB,MAAM,WACJ,OAAO,SAAS,SACZ,OAAO,WACN,OAAO,YAAY,yBAAyB,KAAK;CACxD,MAAM,OACJ,OAAO,SAAS,SACZ;EAAE,MAAM,OAAO;EAAO,WAAW;CAAS,IAC1C;EAAE,KAAK,OAAO;EAAO,WAAW;CAAS;CAE/C,IAAI,KAAK,SAAS,SAAS;EACzB,MAAM,aAAc,KAAK,UACrB;EACJ,OAAO;GAAE,MAAM;GAAS,GAAG;GAAM,GAAI,cAAc,EAAE,WAAW;EAAG;CACrE;CACA,OAAO;EAAE,MAAM,KAAK;EAAM,GAAG;CAAK;AACpC;;;;;;;AAQA,SAAS,uBACP,UACwB;CACxB,MAAM,QAAgC,CAAC;CACvC,KAAK,MAAM,OAAO,UAAU;EAC1B,IAAI,IAAI,SAAS,QAAQ;EACzB,MAAM,WAAW,IAAI,SAAS,cAAc,iBAAiB;EAC7D,MAAM,UAAqC,CAAC;EAC5C,IAAI,MAAM,QAAQ,IAAI,OAAO,GAC3B,KAAK,MAAM,QAAQ,IAAI,SACrB,QAAQ,KAAK,yBAAyB,IAAI,CAAC;OAExC,IAAI,IAAI,SACb,QAAQ,KAAK;GAAE,MAAM;GAAQ,MAAM,IAAI;EAAQ,CAAC;EAElD,IAAI,QAAQ,SAAS,GACnB,MAAM,KAAK;GAAE,MAAM;GAAU;EAAQ,CAAC;CAE1C;CACA,OAAO;AACT;;;;;;;AAqDA,IAAa,oBAAb,cAOU,gBAOR;CACA,OAAyB;CACzB,OAAgB;CAEhB;CAEA,YAAY,QAA0B,OAAe;EACnD,MAAM,CAAC,GAAG,KAAK;EACf,KAAK,SAAS,mBAAmB,MAAM;CACzC;CAEA,OAAO,WACL,SACkC;EAGlC,IAAI,gBAAgB,QAAQ,QAAQ,GAAG;GACrC,OAAO,KAAK,mBAAmB,OAAO;GACtC;EACF;EAEA,MAAM,gBAAgB,KAAK,yBAAyB,OAAO;EAC3D,MAAM,EAAE,WAAW;EAEnB,IAAI;GACF,OAAO,QACL,uCAAuC,KAAK,MAAM,YAAY,QAAQ,SAAS,OAAO,SAAS,QAAQ,OAAO,UAAU,EAAE,eAC1H;IAAE,UAAU;IAAU,OAAO,KAAK;GAAM,CAC1C;GACA,MAAM,SACJ,MAAM,KAAK,OAAO,OAAO,sBAAsB,aAAa;GAE9D,OAAO,KAAK,oBAAoB,QAAQ,SAAS,MAAM;EACzD,SAAS,OAAO;GACd,MAAM,WAAW,mBAAmB,KAAK;GACzC,OAAO,OAAO,2BAA2B;IACvC;IACA,QAAQ;GACV,CAAC;GACD,MAAM;IACJ,MAAM,UAAU;IAChB,OAAO,QAAQ;IACf,WAAW,KAAK,IAAI;IACpB,SACE,iBAAiB,QACb,MAAM,UACN;IAGN,GAAI,aAAa,KAAA,KAAa,EAAE,SAAS;IACzC,OAAO,EACL,SACE,iBAAiB,QACb,MAAM,UACN,oDACR;GACF;EACF;CACF;;;;;;;;;CAUA,OAAe,mBACb,SACkC;EAClC,MAAM,QAAQ,QAAQ;EACtB,MAAM,EAAE,WAAW;EACnB,MAAM,QAAQ,QAAQ,SAAS,WAAW,KAAK,IAAI;EACnD,MAAM,WAAW,QAAQ,YAAY,WAAW,KAAK,IAAI;EACzD,MAAM,YAAY,WAAW,KAAK,IAAI;EAEtC,IAAI;GACF,OAAO,QACL,uCAAuC,MAAM,YAAY,QAAQ,SAAS,OAAO,mCACjF;IAAE,UAAU;IAAU;GAAM,CAC9B;GAEA,MAAM,oBAAoB,uBAAuB,QAAQ,aAAa;GACtE,MAAM,oBACJ,kBAAkB,SAAS,IACvB,kBAAkB,KAAK,MAAM,EAAE,OAAO,CAAC,CAAC,KAAK,IAAI,IACjD,KAAA;GAEN,MAAM,QAAQ,uBAAuB,QAAQ,QAAQ;GAarD,MAAM,QAAO,MARa,KAAK,OAAO,aAAa,OAAO;IACxD;IACA,GAAI,sBAAsB,KAAA,KAAa,EACrC,oBAAoB,kBACtB;IACO;GACT,CAAC,EAAA,CAEwB,eAAe;GAExC,MAAM;IACJ,MAAM,UAAU;IAChB;IACA;IACA;IACA,WAAW,KAAK,IAAI;IACpB,aAAa,QAAQ;GACvB;GACA,MAAM;IACJ,MAAM,UAAU;IAChB;IACA;IACA,WAAW,KAAK,IAAI;IACpB,MAAM;GACR;GACA,IAAI,MACF,MAAM;IACJ,MAAM,UAAU;IAChB;IACA;IACA,WAAW,KAAK,IAAI;IACpB,OAAO;IACP,SAAS;GACX;GAEF,MAAM;IACJ,MAAM,UAAU;IAChB;IACA;IACA,WAAW,KAAK,IAAI;GACtB;GACA,MAAM;IACJ,MAAM,UAAU;IAChB;IACA;IACA;IACA,WAAW,KAAK,IAAI;IACpB,cAAc;GAChB;EACF,SAAS,OAAO;GACd,MAAM,WAAW,mBAAmB,KAAK;GACzC,OAAO,OAAO,mCAAmC;IAC/C;IACA,QAAQ;GACV,CAAC;GACD,MAAM;IACJ,MAAM,UAAU;IAChB;IACA,WAAW,KAAK,IAAI;IACpB,SACE,iBAAiB,QACb,MAAM,UACN;IACN,GAAI,aAAa,KAAA,KAAa,EAAE,SAAS;IACzC,OAAO,EACL,SACE,iBAAiB,QACb,MAAM,UACN,oDACR;GACF;EACF;CACF;;;;;;CAOA,MAAM,iBACJ,SAC0C;EAC1C,MAAM,EAAE,aAAa,iBAAiB;EACtC,MAAM,EAAE,WAAW;EAEnB,MAAM,gBAAgB,KAAK,yBAAyB,WAAW;EAE/D,IAAI;GACF,OAAO,QACL,uCAAuC,KAAK,MAAM,YAAY,YAAY,SAAS,OAAO,SAAS,YAAY,OAAO,UAAU,EAAE,gBAClI;IAAE,UAAU;IAAU,OAAO,KAAK;GAAM,CAC1C;GAEA,MAAM,SAAS,MAAM,KAAK,OAAO,OAAO,gBAAgB;IACtD,GAAG;IACH,QAAQ;KACN,GAAG,cAAc;KACjB,kBAAkB;KAClB,gBAAgB;IAClB;GACF,CAAC;GAGD,MAAM,UAAU,KAAK,wBAAwB,MAAM;GAGnD,IAAI;GACJ,IAAI;IACF,SAAS,KAAK,MAAM,OAAO;GAC7B,QAAQ;IACN,MAAM,IAAI,MAAM,sBAAsB,SAAS,mBAAmB,CAAC;GACrE;GAEA,OAAO;IACL,MAAM;IACN;IACA,OAAO,OAAO,gBACV,iBAAiB,OAAO,aAAa,IACrC,KAAA;GACN;EACF,SAAS,OAAO;GACd,OAAO,OAAO,iCAAiC;IAC7C;IACA,QAAQ;GACV,CAAC;GACD,MAAM,IAAI,MACR,iBAAiB,QACb,MAAM,UACN,gEACN;EACF;CACF;;;;;;;;CASA,OAAO,uBACL,SACkC;EAClC,MAAM,EAAE,aAAa,sBAAsB,iBAAiB;EAC5D,MAAM,cAAc;GAClB,GAAG;GACH,OAAO,qBAAqB,SAAS,WAAW,KAAK,IAAI;EAC3D;EACA,MAAM,gBAAgB,KAAK,yBAAyB,WAAW;EAE/D,IAAI;GACF,YAAY,OAAO,QACjB,yDAAyD,KAAK,MAAM,YAAY,YAAY,SAAS,UACrG;IAAE,UAAU;IAAU,OAAO,KAAK;GAAM,CAC1C;GACA,MAAM,SAAS,MAAM,KAAK,OAAO,OAAO,sBAAsB;IAC5D,GAAG;IACH,QAAQ;KACN,GAAG,cAAc;KACjB,kBAAkB;KAClB,gBAAgB;IAClB;GACF,CAAC;GAED,IAAI,UAAU;GACd,IAAI;GAGJ,IAAI,SAAS;GACb,WAAW,MAAM,SAAS,KAAK,oBAC7B,QACA,aACA,YAAY,MACd,GAAG;IACD,IAAI,MAAM,SAAS,UAAU,sBAC3B,WAAW,MAAM;IAEnB,IAAI,MAAM,SAAS,UAAU,WAAW,SAAS;IACjD,IAAI,MAAM,SAAS,UAAU,cAC3B,WAAW;SAEX,MAAM;GAEV;GAEA,IAAI,QAAQ;GACZ,IAAI,CAAC,UAAU;IACb,MAAM,sBACJ,aACA,kEACA,kBACF;IACA;GACF;GACA,IAAI,CAAC,SAAS;IACZ,MAAM,sBACJ,aACA,wDACA,gBACF;IACA;GACF;GAEA,IAAI;GACJ,IAAI;IACF,SAAS,KAAK,MAAM,OAAO;GAC7B,QAAQ;IACN,MAAM,sBACJ,aACA,sBAAsB,SAAS,iCAAiC,GAChE,aACF;IACA;GACF;GAEA,MAAM;IACJ,MAAM,UAAU;IAChB,MAAM;IACN,OAAO;KAAE;KAAQ,KAAK;IAAQ;IAC9B,OAAO,YAAY;IACnB,WAAW,KAAK,IAAI;GACtB;GACA,MAAM;IAAE,GAAG;IAAU,WAAW,KAAK,IAAI;GAAE;EAC7C,SAAS,OAAO;GACd,MAAM,WAAW,mBAAmB,KAAK;GACzC,MAAM,UACJ,iBAAiB,QACb,MAAM,UACN;GACN,YAAY,OAAO,OAAO,uCAAuC;IAC/D;IACA,QAAQ;GACV,CAAC;GACD,MAAM;IACJ,GAAG,sBAAsB,aAAa,SAAS,gBAAgB;IAC/D,GAAI,aAAa,KAAA,KAAa,EAAE,SAAS;GAC3C;EACF;CACF;;;;CAKA,wBAAgC,UAA2C;EACzE,IAAI,cAAc;EAElB,IAAI,SAAS,aAAa,EAAE,EAAE,SAAS,OAChC;QAAA,MAAM,QAAQ,SAAS,WAAW,EAAE,CAAC,QAAQ,OAChD,IAAI,KAAK,MACP,eAAe,KAAK;EAAA;EAK1B,OAAO;CACT;CAEA,OAAe,oBACb,QACA,SACA,QACkC;EAClC,MAAM,QAAQ,QAAQ;EACtB,IAAI,qBAAqB;EACzB,IAAI,sBAAsB;EAC1B,MAAM,8BAAc,IAAI,IAStB;EACF,IAAI,gBAAgB;EAGpB,MAAM,QAAQ,QAAQ,SAAS,WAAW,KAAK,IAAI;EACnD,MAAM,WAAW,QAAQ,YAAY,WAAW,KAAK,IAAI;EACzD,MAAM,YAAY,WAAW,KAAK,IAAI;EACtC,IAAI,SAAwB;EAC5B,IAAI,qBAAoC;EACxC,IAAI,qBAAqB;EACzB,IAAI,uBAAuB;EAC3B,IAAI,6BAA6B;EACjC,IAAI,wBAAwB;EAE5B,WAAW,MAAM,SAAS,QAAQ;GAChC,OAAO,SAAS,mBAAmB,EAAE,MAAM,CAAC;GAE5C,IAAI,CAAC,sBAAsB;IACzB,uBAAuB;IACvB,MAAM;KACJ,MAAM,UAAU;KAChB;KACA;KACA;KACA,WAAW,KAAK,IAAI;KACpB,aAAa,QAAQ;IACvB;GACF;GAEA,IAAI,MAAM,aAAa,EAAE,EAAE,SAAS,OAAO;IACzC,MAAM,QAAQ,MAAM,WAAW,EAAE,CAAC,QAAQ;IAE1C,KAAK,MAAM,QAAQ,OAAO;KACxB,IAAI,KAAK,MAAM;MACb,IAAI,KAAK,SAAS;OAEhB,IAAI,CAAC,uBAAuB;QAC1B,wBAAwB;QACxB,SAAS,WAAW,KAAK,IAAI;QAC7B,qBAAqB,WAAW,KAAK,IAAI;QAGzC,MAAM;SACJ,MAAM,UAAU;SAChB,WAAW;SACX;SACA,WAAW,KAAK,IAAI;QACtB;QACA,MAAM;SACJ,MAAM,UAAU;SAChB,WAAW;SACX,MAAM;SACN;SACA,WAAW,KAAK,IAAI;QACtB;QAGA,MAAM;SACJ,MAAM,UAAU;SAChB,UAAU;SACV;SACA;SACA,WAAW,KAAK,IAAI;SACpB,UAAU;QACZ;OACF;OAEA,uBAAuB,KAAK;OAI5B,IAAI,CAAC,oBAAoB;OACzB,MAAM;QACJ,MAAM,UAAU;QAChB,WAAW;QACX,OAAO,KAAK;QACZ;QACA,WAAW,KAAK,IAAI;OACtB;OAGA,MAAM;QACJ,MAAM,UAAU;QAChB,UAAU,UAAU,WAAW,KAAK,IAAI;QACxC,QAAQ,UAAU,WAAW,KAAK,IAAI;QACtC;QACA,WAAW,KAAK,IAAI;QACpB,OAAO,KAAK;QACZ,SAAS;OACX;MACF,OAAO,IAAI,KAAK,KAAK,KAAK,GAAG;OAE3B,IAAI,sBAAsB,CAAC,oBAAoB;QAC7C,qBAAqB;QACrB,MAAM;SACJ,MAAM,UAAU;SAChB,WAAW;SACX;SACA,WAAW,KAAK,IAAI;QACtB;QACA,MAAM;SACJ,MAAM,UAAU;SAChB,WAAW;SACX;SACA,WAAW,KAAK,IAAI;QACtB;OACF;OAIA,IAAI,CAAC,4BAA4B;QAC/B,6BAA6B;QAC7B,MAAM;SACJ,MAAM,UAAU;SAChB;SACA;SACA,WAAW,KAAK,IAAI;SACpB,MAAM;QACR;OACF;OAEA,sBAAsB,KAAK;OAC3B,MAAM;QACJ,MAAM,UAAU;QAChB;QACA;QACA,WAAW,KAAK,IAAI;QACpB,OAAO,KAAK;QACZ,SAAS;OACX;MACF;KACF;KAEA,MAAM,eAAe,KAAK;KAC1B,IAAI,cAAc;MAChB,MAAM,aACJ,aAAa,MACb,GAAG,aAAa,KAAK,GAAG,KAAK,IAAI,EAAE,GAAG;MACxC,MAAM,eAAe,aAAa,QAAQ,CAAC;MAK3C,MAAM,uBAAuB,KAAK,oBAAoB,KAAA;MAEtD,IAAI,eAAe,YAAY,IAAI,UAAU;MAC7C,IAAI,CAAC,cAAc;OACjB,eAAe;QACb,MAAM,aAAa,QAAQ;QAC3B,MACE,OAAO,iBAAiB,WACpB,eACA,KAAK,UAAU,YAAY;QACjC,OAAO;QACP,SAAS;QAGT,GAAI,yBAAyB,KAAA,KAAa,EACxC,kBAAkB,qBACpB;OACF;OACA,YAAY,IAAI,YAAY,YAAY;MAC1C,OAAO;OACL,IAAI,CAAC,aAAa,oBAAoB,sBACpC,aAAa,mBAAmB;OAElC,IAAI;QACF,MAAM,eAAe,KAAK,MAAM,aAAa,IAAI;QACjD,MAAM,UACJ,OAAO,iBAAiB,WACpB,KAAK,MAAM,YAAY,IACvB;QACN,MAAM,aAAa;SAAE,GAAG;SAAc,GAAG;QAAQ;QACjD,aAAa,OAAO,KAAK,UAAU,UAAU;OAC/C,QAAQ;QACN,aAAa,OACX,OAAO,iBAAiB,WACpB,eACA,KAAK,UAAU,YAAY;OACnC;MACF;MAGA,IAAI,CAAC,aAAa,SAAS;OACzB,aAAa,UAAU;OACvB,MAAM;QACJ,MAAM,UAAU;QAChB;QACA,cAAc,aAAa;QAC3B,UAAU,aAAa;QACvB,iBAAiB;QACjB;QACA,WAAW,KAAK,IAAI;QACpB,OAAO,aAAa;QACpB,GAAI,aAAa,oBAAoB,EACnC,UAAU,EACR,kBAAkB,aAAa,iBACjC,EACF;OACF;MACF;MAGA,MAAM;OACJ,MAAM,UAAU;OAChB;OACA;OACA,WAAW,KAAK,IAAI;OACpB,OAAO,aAAa;OACpB,MAAM,aAAa;MACrB;KACF;IACF;GACF,OAAO,IAAI,MAAM,QAAQ,MAAM,KAAK,KAAK,GAAG;IAG1C,IAAI,CAAC,4BAA4B;KAC/B,6BAA6B;KAC7B,MAAM;MACJ,MAAM,UAAU;MAChB;MACA;MACA,WAAW,KAAK,IAAI;MACpB,MAAM;KACR;IACF;IAEA,sBAAsB,MAAM;IAC5B,MAAM;KACJ,MAAM,UAAU;KAChB;KACA;KACA,WAAW,KAAK,IAAI;KACpB,OAAO,MAAM;KACb,SAAS;IACX;GACF;GAEA,IAAI,MAAM,aAAa,EAAE,EAAE,cAAc;IACvC,MAAM,eAAe,MAAM,WAAW,EAAE,CAAC;IAKzC,KAAK,MAAM,CAAC,YAAY,iBAAiB,YAAY,QAAQ,GAAG;KAC9D,IAAI,cAAuB,CAAC;KAC5B,IAAI;MACF,MAAM,SAAS,KAAK,MAAM,aAAa,IAAI;MAC3C,cAAc,UAAU,OAAO,WAAW,WAAW,SAAS,CAAC;KACjE,QAAQ;MACN,cAAc,CAAC;KACjB;KAEA,MAAM;MACJ,MAAM,UAAU;MAChB;MACA,cAAc,aAAa;MAC3B,UAAU,aAAa;MACvB;MACA,WAAW,KAAK,IAAI;MACpB,OAAO;KACT;IACF;IAGA,IAAI,YAAY,OAAO,GACrB,6BAA6B;IAG/B,IAAI,iBAAiB,aAAa,YAChC,MAAM;KACJ,MAAM,UAAU;KAChB;KACA;KACA,WAAW,KAAK,IAAI;KACpB,SACE;KACF,MAAM;KACN,OAAO;MACL,SACE;MACF,MAAM;KACR;IACF;IAIF,IAAI,sBAAsB,CAAC,oBAAoB;KAC7C,qBAAqB;KACrB,MAAM;MACJ,MAAM,UAAU;MAChB,WAAW;MACX;MACA,WAAW,KAAK,IAAI;KACtB;KACA,MAAM;MACJ,MAAM,UAAU;MAChB,WAAW;MACX;MACA,WAAW,KAAK,IAAI;KACtB;IACF;IAGA,IAAI,4BACF,MAAM;KACJ,MAAM,UAAU;KAChB;KACA;KACA,WAAW,KAAK,IAAI;IACtB;IAGF,MAAM;KACJ,MAAM,UAAU;KAChB;KACA;KACA;KACA,WAAW,KAAK,IAAI;KACpB,cAAc,YAAY,OAAO,IAAI,eAAe;KAIpD,GAAI,MAAM,iBAAiB,EACzB,OAAO,iBAAiB,MAAM,aAAa,EAC7C;IACF;GACF;EACF;CACF;CAEA,2BAAmC,MAAyB;EAC1D,QAAQ,KAAK,MAAb;GACE,KAAK,QACH,OAAO,EAAE,MAAM,KAAK,QAAQ;GAC9B,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK,YAAY;IACf,MAAM,aACJ,KAAK,OAAO,SAAS,SACjB,EACE,YAAY;KACV,MAAM,KAAK,OAAO;KAClB,UAAU,KAAK,OAAO;IACxB,EACF,IACA,EACE,UAAU;KACR,SAAS,KAAK,OAAO;KAGrB,UACE,KAAK,OAAO,YAAY,yBAAyB,KAAK;IAC1D,EACF;IAKN,IAAI,KAAK,SAAS,SAAS;KACzB,MAAM,OAAO,KAAK;KAClB,MAAM,gBAA+B;MACnC,GAAI,MAAM,QAAQ,KAAA,KAAa,EAAE,KAAK,KAAK,IAAI;MAC/C,GAAI,MAAM,gBAAgB,KAAA,KAAa,EACrC,aAAa,KAAK,YACpB;MACA,GAAI,MAAM,cAAc,KAAA,KAAa,EAAE,WAAW,KAAK,UAAU;KACnE;KACA,IAAI,OAAO,KAAK,aAAa,CAAC,CAAC,SAAS,GACtC,WAAW,gBAAgB;IAE/B;IAEA,OAAO;GACT;GACA,SAEE,MAAM,IAAI,MACR,kCAAmC,KAAiC,MACtE;EAEJ;CACF;CAEA,eACE,UACuC;EAGvC,MAAM,mCAAmB,IAAI,IAAoB;EACjD,KAAK,MAAM,OAAO,UAChB,IAAI,IAAI,SAAS,eAAe,IAAI,WAClC,KAAK,MAAM,MAAM,IAAI,WACnB,iBAAiB,IAAI,GAAG,IAAI,GAAG,SAAS,IAAI;EAKlD,MAAM,YAAY,SAAS,KAAK,QAAQ;GACtC,MAAM,OAAyB,IAAI,SAAS,cAAc,UAAU;GACpE,MAAM,QAAqB,CAAC;GAE5B,IAAI,MAAM,QAAQ,IAAI,OAAO,GAC3B,KAAK,MAAM,eAAe,IAAI,SAC5B,MAAM,KAAK,KAAK,2BAA2B,WAAW,CAAC;QAEpD,IAAI,IAAI,WAAW,IAAI,SAAS,QACrC,MAAM,KAAK,EAAE,MAAM,IAAI,QAAQ,CAAC;GAGlC,IAAI,IAAI,SAAS,eAAe,IAAI,WAAW,QAC7C,KAAK,MAAM,YAAY,IAAI,WAAW;IACpC,IAAI,aAAsC,CAAC;IAC3C,IAAI;KACF,aAAa,SAAS,SAAS,YAC1B,KAAK,MAAM,SAAS,SAAS,SAAS,IAIvC,CAAC;IACP,QAAQ;KACN,aAAa,CAAC;IAChB;IAEA,MAAM,mBACJ,SAAS,UACR;IAKH,MAAM,OAAa,EACjB,cAAc;KACZ,IAAI,SAAS;KACb,MAAM,SAAS,SAAS;KACxB,MAAM;IACR,EACF;IACA,IAAI,kBACF,KAAK,mBAAmB;IAE1B,MAAM,KAAK,IAAI;GACjB;GAGF,IAAI,IAAI,SAAS,UAAU,IAAI,YAAY;IACzC,MAAM,eACJ,iBAAiB,IAAI,IAAI,UAAU,KAAK,IAAI;IAC9C,MAAM,cAAc,IAAI;IACxB,IAAI,MAAM,QAAQ,WAAW,GAAG;KAC9B,MAAM,aAA4B,CAAC;KACnC,MAAM,aAA0B,CAAC;KACjC,KAAK,MAAM,QAAQ,aACjB,IAAI,KAAK,SAAS,QAChB,WAAW,KAAK,KAAK,OAAO;UACvB,IAAI,KAAK,OAAO,SAAS,QAC9B,WAAW,KAAK,EACd,YAAY;MACV,MAAM,KAAK,OAAO;MAClB,UAAU,KAAK,OAAO;KACxB,EACF,CAAC;UACI;MACL,MAAM,kBAAkB;OACtB,OAAO;OACP,OAAO;OACP,OAAO;OACP,UAAU;MACZ,EAAE,KAAK;MACP,WAAW,KAAK,EACd,UAAU;OACR,SAAS,KAAK,OAAO;OACrB,UAAU,KAAK,OAAO,YAAY;MACpC,EACF,CAAC;KACH;KAEF,MAAM,KAAK,EACT,kBAAkB;MAChB,IAAI,IAAI;MACR,MAAM;MACN,UAAU,EAAE,SAAS,WAAW,KAAK,IAAI,EAAE;MAC3C,GAAI,WAAW,SAAS,KAAK,EAAE,OAAO,WAAW;KACnD,EACF,CAAC;IACH,OACE,MAAM,KAAK,EACT,kBAAkB;KAChB,IAAI,IAAI;KACR,MAAM;KACN,UAAU,EAAE,SAAS,eAAe,GAAG;IACzC,EACF,CAAC;GAEL;GAEA,OAAO;IACL;IACA,OAAO,MAAM,SAAS,IAAI,QAAQ,CAAC,EAAE,MAAM,GAAG,CAAC;GACjD;EACF,CAAC;EAKD,OAAO,KAAK,iCAAiC,SAAS;CACxD;;;;;;;;;;CAWA,iCACE,UACgB;EAChB,MAAM,SAAyB,CAAC;EAEhC,KAAK,MAAM,OAAO,UAAU;GAC1B,MAAM,QAAQ,IAAI,SAAS,CAAC;GAG5B,IAAI,IAAI,SAAS,SAMX;QAAA,EAJF,MAAM,SAAS,KACf,CAAC,MAAM,OACJ,MAAM,UAAU,KAAM,EAAuB,SAAS,EACzD,IAEA;GAAA;GAIJ,MAAM,OAAO,OAAO,OAAO,SAAS;GACpC,IAAI,QAAQ,KAAK,SAAS,IAAI,MAE5B,KAAK,QAAQ,CAAC,GAAI,KAAK,SAAS,CAAC,GAAI,GAAG,KAAK;QAE7C,OAAO,KAAK;IAAE,GAAG;IAAK,OAAO,CAAC,GAAG,KAAK;GAAE,CAAC;EAE7C;EAGA,KAAK,MAAM,OAAO,QAAQ;GACxB,IAAI,CAAC,IAAI,OAAO;GAChB,MAAM,4CAA4B,IAAI,IAAY;GAClD,IAAI,QAAQ,IAAI,MAAM,QAAQ,SAAS;IACrC,IAAI,sBAAsB,QAAQ,KAAK,kBAAkB,MAAM;KAC7D,IAAI,0BAA0B,IAAI,KAAK,iBAAiB,IAAI,GAC1D,OAAO;KAET,0BAA0B,IAAI,KAAK,iBAAiB,IAAI;IAC1D;IACA,OAAO;GACT,CAAC;EACH;EAEA,OAAO;CACT;CAEA,yBACE,SACA;EAKA,MAAM,EAAE,gBAAgB,GAAG,cAAc,QAAQ,gBAAgB,CAAC;EAMlE,MAAM,uBAAuB,iBACzB;GACE,GAAI,eAAe,oBAAoB,KAAA,KAAa,EAClD,iBAAiB,eAAe,gBAClC;GACA,GAAI,eAAe,mBAAmB,KAAA,KAAa,EACjD,gBAAgB,eAAe,eACjC;GACA,GAAI,eAAe,gBACf,EACE,eAAe,eAAe,cAChC,IACA,CAAC;EACP,IACA,KAAA;EAEJ,MAAM,oBAAoB,uBAAuB,QAAQ,aAAa;EACtE,MAAM,oBACJ,kBAAkB,SAAS,IACvB,kBAAkB,KAAK,MAAM,EAAE,OAAO,CAAC,CAAC,KAAK,IAAI,IACjD,KAAA;EAWN,MAAM,iBAAiB,QAAQ;EAG/B,MAAM,uBAAuB,iBACzB;GACE,kBAAkB;GAClB,gBAAgB;EAClB,IACA,KAAA;EAwBJ,OAAO;GAlBL,OAAO,QAAQ;GACf,UAAU,KAAK,eAAe,QAAQ,QAAQ;GAC9C,QAAQ;IACN,GAAG;IACH,GAAI,yBAAyB,KAAA,KAAa,EACxC,gBAAgB,qBAClB;IACA,GAAI,sBAAsB,KAAA,KAAa,EAAE,kBAAkB;IAC3D,OAAO,6BAA6B,QAAQ,KAAK;IACjD,GAAI,wBAAwB,CAAC;IAG7B,GAAI,QAAQ,SAAS,UAAU,QAAQ,EACrC,aAAa,QAAQ,QAAQ,OAC/B;GACF;EAGK;CACT;;;;;;;CAQA,iCAA0C;EACxC,OAAO,wCAAwC,IAAI,KAAK,KAAK;CAC/D;AACF;AAEA,SAAS,sBAAsB,SAAiB,OAAe;CAG7D,OAAO,mBAAmB,MAAM,qBAFhB,QAAQ,MAAM,GAAG,GAEoB,IADpC,QAAQ,SAAS,MAAM,QAAQ;AAElD;AAEA,SAAS,sBACP,SACA,SACA,MACkE;CAClE,OAAO;EACL,MAAM,UAAU;EAChB,OAAO,QAAQ;EACf,OAAO,QAAQ;EACf,WAAW,KAAK,IAAI;EACpB;EACA;EACA,OAAO;GAAE;GAAS;EAAK;CACzB;AACF;;;;;AAMA,SAAgB,iBACd,OACA,QACA,QAMA;CACA,OAAO,IAAI,kBAAkB;EAAE;EAAQ,GAAG;CAAO,GAAG,KAAK;AAC3D;;;;;AAMA,SAAgB,WACd,OACA,QAMA;CAEA,OAAO,iBAAiB,OADT,uBACgB,GAAQ,MAAM;AAC/C"}
|
|
1
|
+
{"version":3,"file":"text.js","names":[],"sources":["../../../src/adapters/text.ts"],"sourcesContent":["import { FinishReason } from '@google/genai'\nimport { EventType, normalizeSystemPrompts } from '@tanstack/ai'\nimport { toRunErrorRawEvent } from '@tanstack/ai/adapter-internals'\nimport { BaseTextAdapter } from '@tanstack/ai/adapters'\nimport { convertToolsToProviderFormat } from '../tools/tool-converter'\nimport { buildGeminiUsage } from '../usage'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport { GEMINI_COMBINED_TOOLS_AND_SCHEMA_MODELS } from '../model-meta'\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 VideoMetadata,\n} from '@google/genai'\nimport type {\n ContentPart,\n Modality,\n ModelMessage,\n AdapterYieldChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type { ExternalTextProviderOptions } from '../text/text-provider-options'\nimport type {\n GeminiMessageMetadataByModality,\n GeminiToolCallMetadata,\n GeminiVideoMetadata,\n GeminiVideoProcessing,\n} from '../message-types'\nimport type { GeminiClientConfig } from '../utils/client'\n\n/**\n * Fallback MIME types for URL-sourced media parts that don't specify one.\n */\nconst DEFAULT_MEDIA_MIME_TYPES = {\n image: 'image/jpeg',\n audio: 'audio/mp3',\n video: 'video/mp4',\n document: 'application/pdf',\n} as const\n\n/**\n * Content block shape for an Interactions API `input` step. The installed\n * @google/genai types predate the video `processing` field, so we model the\n * subset we emit and cast at the call site.\n */\ntype InteractionContent =\n | { type: 'text'; text: string }\n | {\n type: 'video'\n uri?: string\n data?: string\n mime_type?: string\n processing?: GeminiVideoProcessing\n }\n | {\n type: 'image' | 'audio' | 'document'\n uri?: string\n data?: string\n mime_type?: string\n }\n\ninterface InteractionStep {\n type: 'user_input' | 'model_output'\n content: Array<InteractionContent>\n}\n\n/** True when any message carries a video part requesting agentic processing. */\nfunction hasAgenticVideo(messages: Array<ModelMessage>): boolean {\n return messages.some(\n (msg) =>\n Array.isArray(msg.content) &&\n msg.content.some(\n (part) =>\n part.type === 'video' &&\n (part.metadata as GeminiVideoMetadata | undefined)?.processing ===\n 'agentic',\n ),\n )\n}\n\n/** Convert a single content part to an Interactions API content block. */\nfunction contentPartToInteraction(part: ContentPart): InteractionContent {\n if (part.type === 'text') {\n return { type: 'text', text: part.content }\n }\n\n const source = part.source\n const mimeType =\n source.type === 'data'\n ? source.mimeType\n : (source.mimeType ?? DEFAULT_MEDIA_MIME_TYPES[part.type])\n const base =\n source.type === 'data'\n ? { data: source.value, mime_type: mimeType }\n : { uri: source.value, mime_type: mimeType }\n\n if (part.type === 'video') {\n const processing = (part.metadata as GeminiVideoMetadata | undefined)\n ?.processing\n return { type: 'video', ...base, ...(processing && { processing }) }\n }\n return { type: part.type, ...base }\n}\n\n/**\n * Build the Interactions API `input` from chat messages. Each user/assistant\n * message becomes a `user_input` / `model_output` step wrapping its content\n * blocks — the wrapping the Python SDK performs implicitly but the JS SDK\n * does not. Tool messages are skipped (unsupported on this path).\n */\nfunction buildInteractionsInput(\n messages: Array<ModelMessage>,\n): Array<InteractionStep> {\n const steps: Array<InteractionStep> = []\n for (const msg of messages) {\n if (msg.role === 'tool') continue\n const stepType = msg.role === 'assistant' ? 'model_output' : 'user_input'\n const content: Array<InteractionContent> = []\n if (Array.isArray(msg.content)) {\n for (const part of msg.content) {\n content.push(contentPartToInteraction(part))\n }\n } else if (msg.content) {\n content.push({ type: 'text', text: msg.content })\n }\n if (content.length > 0) {\n steps.push({ type: stepType, content })\n }\n }\n return steps\n}\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 override readonly kind = 'text' as const\n readonly name = 'gemini' as const\n\n private readonly 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<AdapterYieldChunk> {\n // Agentic video understanding is only exposed through the Interactions API,\n // not generateContent. Detect it and take that path instead.\n if (hasAgenticVideo(options.messages)) {\n yield* this.interactionsStream(options)\n return\n }\n\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 rawEvent = toRunErrorRawEvent(error)\n logger.errors('gemini.chatStream fatal', {\n error,\n source: 'gemini.chatStream',\n })\n yield {\n type: EventType.RUN_ERROR,\n model: options.model,\n timestamp: Date.now(),\n message:\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during the chat stream.',\n // Forward the provider's structured error body when present (see\n // toRunErrorRawEvent); omitted otherwise.\n ...(rawEvent !== undefined && { rawEvent }),\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 * Agentic video-understanding path via the Interactions API.\n *\n * The Interactions API (unlike `generateContent`) requires message parts to\n * be wrapped in `user_input` / `model_output` steps, and it accepts the\n * `processing: 'agentic'` video flag. This is a non-streaming call whose\n * single text result is re-emitted as AG-UI stream chunks.\n */\n private async *interactionsStream(\n options: TextOptions<GeminiTextProviderOptions>,\n ): AsyncIterable<AdapterYieldChunk> {\n const model = options.model\n const { logger } = options\n const runId = options.runId ?? generateId(this.name)\n const threadId = options.threadId ?? generateId(this.name)\n const messageId = generateId(this.name)\n\n try {\n logger.request(\n `activity=chat provider=gemini model=${model} messages=${options.messages.length} mode=interactions-agentic-video`,\n { provider: 'gemini', model },\n )\n\n const normalizedPrompts = normalizeSystemPrompts(options.systemPrompts)\n const systemInstruction =\n normalizedPrompts.length > 0\n ? normalizedPrompts.map((p) => p.content).join('\\n')\n : undefined\n\n const input = buildInteractionsInput(options.messages)\n\n // The installed @google/genai (2.10.0) Interactions `VideoContent` type\n // predates the `processing` field, so the structurally-built input is\n // cast at the call boundary. The SDK forwards it to the wire unchanged.\n const interaction = await this.client.interactions.create({\n model,\n ...(systemInstruction !== undefined && {\n system_instruction: systemInstruction,\n }),\n input: input as never,\n })\n\n const text = interaction.output_text ?? ''\n\n yield {\n type: EventType.RUN_STARTED,\n runId,\n threadId,\n model,\n timestamp: Date.now(),\n parentRunId: options.parentRunId,\n }\n yield {\n type: EventType.TEXT_MESSAGE_START,\n messageId,\n model,\n timestamp: Date.now(),\n role: 'assistant',\n }\n if (text) {\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId,\n model,\n timestamp: Date.now(),\n delta: text,\n content: text,\n }\n }\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId,\n model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: Date.now(),\n finishReason: 'stop',\n }\n } catch (error) {\n const rawEvent = toRunErrorRawEvent(error)\n logger.errors('gemini.interactionsStream fatal', {\n error,\n source: 'gemini.interactionsStream',\n })\n yield {\n type: EventType.RUN_ERROR,\n model,\n timestamp: Date.now(),\n message:\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during the chat stream.',\n ...(rawEvent !== undefined && { rawEvent }),\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(jsonContentParseError(rawText, 'structured output'))\n }\n\n return {\n data: parsed,\n rawText,\n usage: result.usageMetadata\n ? buildGeminiUsage(result.usageMetadata)\n : undefined,\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 * Stream schema-constrained JSON from Gemini natively.\n *\n * `chat({ outputSchema, stream: true })` calls this when the adapter\n * implements it. Without it, the engine buffers `structuredOutput()` and\n * emits one synthetic delta.\n */\n async *structuredOutputStream(\n options: StructuredOutputOptions<GeminiTextProviderOptions>,\n ): AsyncIterable<AdapterYieldChunk> {\n const { chatOptions: requestedChatOptions, outputSchema } = options\n const chatOptions = {\n ...requestedChatOptions,\n runId: requestedChatOptions.runId ?? generateId(this.name),\n }\n const mappedOptions = this.mapCommonOptionsToGemini(chatOptions)\n\n try {\n chatOptions.logger.request(\n `activity=structuredOutputStream provider=gemini model=${this.model} messages=${chatOptions.messages.length}`,\n { provider: 'gemini', model: this.model },\n )\n const result = await this.client.models.generateContentStream({\n ...mappedOptions,\n config: {\n ...mappedOptions.config,\n responseMimeType: 'application/json',\n responseSchema: outputSchema,\n },\n })\n\n let rawText = ''\n let finished:\n | Extract<AdapterYieldChunk, { type: typeof EventType.RUN_FINISHED }>\n | undefined\n let failed = false\n for await (const chunk of this.processStreamChunks(\n result,\n chatOptions,\n chatOptions.logger,\n )) {\n if (chunk.type === EventType.TEXT_MESSAGE_CONTENT) {\n rawText += chunk.delta\n }\n if (chunk.type === EventType.RUN_ERROR) failed = true\n if (chunk.type === EventType.RUN_FINISHED) {\n finished = chunk\n } else {\n yield chunk\n }\n }\n\n if (failed) return\n if (!finished) {\n yield structuredStreamError(\n chatOptions,\n 'Gemini structured-output stream ended without a terminal event',\n 'truncated-stream',\n )\n return\n }\n if (!rawText) {\n yield structuredStreamError(\n chatOptions,\n 'Gemini structured-output stream contained no content',\n 'empty-response',\n )\n return\n }\n\n let object: unknown\n try {\n object = JSON.parse(rawText)\n } catch {\n yield structuredStreamError(\n chatOptions,\n jsonContentParseError(rawText, 'Gemini structured-output stream'),\n 'parse-error',\n )\n return\n }\n\n yield {\n type: EventType.CUSTOM,\n name: 'structured-output.complete',\n value: { object, raw: rawText },\n model: chatOptions.model,\n timestamp: Date.now(),\n }\n yield { ...finished, timestamp: Date.now() }\n } catch (error) {\n const rawEvent = toRunErrorRawEvent(error)\n const message =\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during structured output streaming.'\n chatOptions.logger.errors('gemini.structuredOutputStream fatal', {\n error,\n source: 'gemini.structuredOutputStream',\n })\n yield {\n ...structuredStreamError(chatOptions, message, 'provider-error'),\n ...(rawEvent !== undefined && { rawEvent }),\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<AdapterYieldChunk> {\n const model = options.model\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 {\n type: EventType.RUN_STARTED,\n runId,\n threadId,\n model,\n timestamp: Date.now(),\n parentRunId: options.parentRunId,\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 {\n type: EventType.REASONING_START,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_MESSAGE_START,\n messageId: reasoningMessageId,\n role: 'reasoning' as const,\n model,\n timestamp: Date.now(),\n }\n\n // Legacy STEP events (kept during transition)\n yield {\n type: EventType.STEP_STARTED,\n stepName: stepId,\n stepId,\n model,\n timestamp: Date.now(),\n stepType: 'thinking',\n }\n }\n\n accumulatedThinking += part.text\n\n // Spec REASONING content event — reasoningMessageId is set in the\n // hasEmittedStepStarted block above (entered on the same `part.thought` path)\n if (!reasoningMessageId) continue\n yield {\n type: EventType.REASONING_MESSAGE_CONTENT,\n messageId: reasoningMessageId,\n delta: part.text,\n model,\n timestamp: Date.now(),\n }\n\n // Legacy STEP event\n yield {\n type: EventType.STEP_FINISHED,\n stepName: stepId || generateId(this.name),\n stepId: stepId || generateId(this.name),\n model,\n timestamp: Date.now(),\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 {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\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 {\n type: EventType.TEXT_MESSAGE_START,\n messageId,\n model,\n timestamp: Date.now(),\n role: 'assistant',\n }\n }\n\n accumulatedContent += part.text\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId,\n model,\n timestamp: Date.now(),\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 // Only set thoughtSignature when present — under EOPT, the\n // optional field cannot accept an explicit `undefined`.\n ...(partThoughtSignature !== undefined && {\n thoughtSignature: partThoughtSignature,\n }),\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 {\n type: EventType.TOOL_CALL_START,\n toolCallId,\n toolCallName: toolCallData.name,\n toolName: toolCallData.name,\n parentMessageId: messageId,\n model,\n timestamp: Date.now(),\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 {\n type: EventType.TOOL_CALL_ARGS,\n toolCallId,\n model,\n timestamp: Date.now(),\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 {\n type: EventType.TEXT_MESSAGE_START,\n messageId,\n model,\n timestamp: Date.now(),\n role: 'assistant',\n }\n }\n\n accumulatedContent += chunk.data\n yield {\n type: EventType.TEXT_MESSAGE_CONTENT,\n messageId,\n model,\n timestamp: Date.now(),\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 // Emit TOOL_CALL_END for all tracked tool calls. functionCall parts on\n // this chunk (including UNEXPECTED_TOOL_CALL finishes) were already\n // registered and started by the per-part loop above.\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 {\n type: EventType.TOOL_CALL_END,\n toolCallId,\n toolCallName: toolCallData.name,\n toolName: toolCallData.name,\n model,\n timestamp: Date.now(),\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 {\n type: EventType.RUN_ERROR,\n runId,\n model,\n timestamp: Date.now(),\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 {\n type: EventType.REASONING_MESSAGE_END,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\n }\n yield {\n type: EventType.REASONING_END,\n messageId: reasoningMessageId,\n model,\n timestamp: Date.now(),\n }\n }\n\n // Emit TEXT_MESSAGE_END if we had text content\n if (hasEmittedTextMessageStart) {\n yield {\n type: EventType.TEXT_MESSAGE_END,\n messageId,\n model,\n timestamp: Date.now(),\n }\n }\n\n yield {\n type: EventType.RUN_FINISHED,\n runId,\n threadId,\n model,\n timestamp: Date.now(),\n finishReason: toolCallMap.size > 0 ? 'tool_calls' : 'stop',\n // RunFinishedEvent.usage is `usage?: {...}` (no `| undefined`) under\n // exactOptionalPropertyTypes; only include it when usageMetadata is\n // present rather than assigning an explicit `undefined`.\n ...(chunk.usageMetadata && {\n usage: buildGeminiUsage(chunk.usageMetadata),\n }),\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 const geminiPart: Part =\n part.source.type === 'data'\n ? {\n inlineData: {\n data: part.source.value,\n mimeType: part.source.mimeType,\n },\n }\n : {\n fileData: {\n fileUri: part.source.value,\n // For URL sources, use provided mimeType or fall back to\n // reasonable defaults.\n mimeType:\n part.source.mimeType ?? DEFAULT_MEDIA_MIME_TYPES[part.type],\n },\n }\n\n // Apply single-pass video sampling controls (fps / clip offsets) from\n // the part metadata. `processing: 'agentic'` is handled separately via\n // the Interactions API and never reaches this generateContent path.\n if (part.type === 'video') {\n const meta = part.metadata as GeminiVideoMetadata | undefined\n const videoMetadata: VideoMetadata = {\n ...(meta?.fps !== undefined && { fps: meta.fps }),\n ...(meta?.startOffset !== undefined && {\n startOffset: meta.startOffset,\n }),\n ...(meta?.endOffset !== undefined && { endOffset: meta.endOffset }),\n }\n if (Object.keys(videoMetadata).length > 0) {\n geminiPart.videoMetadata = videoMetadata\n }\n }\n\n return geminiPart\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 = {}\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 const toolContent = msg.content\n if (Array.isArray(toolContent)) {\n const textChunks: Array<string> = []\n const mediaParts: Array<Part> = []\n for (const part of toolContent) {\n if (part.type === 'text') {\n textChunks.push(part.content)\n } else if (part.source.type === 'data') {\n mediaParts.push({\n inlineData: {\n data: part.source.value,\n mimeType: part.source.mimeType,\n },\n })\n } else {\n const defaultMimeType = {\n image: 'image/jpeg',\n audio: 'audio/mp3',\n video: 'video/mp4',\n document: 'application/pdf',\n }[part.type]\n mediaParts.push({\n fileData: {\n fileUri: part.source.value,\n mimeType: part.source.mimeType ?? defaultMimeType,\n },\n })\n }\n }\n parts.push({\n functionResponse: {\n id: msg.toolCallId,\n name: functionName,\n response: { content: textChunks.join('\\n') },\n ...(mediaParts.length > 0 && { parts: mediaParts }),\n },\n })\n } else {\n parts.push({\n functionResponse: {\n id: msg.toolCallId,\n name: functionName,\n response: { content: toolContent || '' },\n },\n })\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 id (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 id (tool call ID).\n // Two parallel calls to the *same* tool share a `name` but have distinct\n // `id`s — keying on `name` dropped every response but the first for\n // same-tool parallel calls, leaving Gemini with fewer response parts\n // than call parts and a 400 on the next request.\n for (const msg of merged) {\n if (!msg.parts) continue\n const seenFunctionResponseIds = new Set<string>()\n msg.parts = msg.parts.filter((part) => {\n if ('functionResponse' in part && part.functionResponse?.id) {\n if (seenFunctionResponseIds.has(part.functionResponse.id)) {\n return false\n }\n seenFunctionResponseIds.add(part.functionResponse.id)\n }\n return true\n })\n }\n\n return merged\n }\n\n private mapCommonOptionsToGemini(\n options: TextOptions<GeminiTextProviderOptions>,\n ) {\n // Separate `thinkingConfig` from the other model options so the loose\n // local `thinkingLevel?: keyof typeof ThinkingLevel` type doesn't leak\n // into the SDK config object via the `...modelOpts` spread — we re-add a\n // properly-typed `ThinkingConfig` below.\n const { thinkingConfig, ...modelOpts } = options.modelOptions ?? {}\n // Build the thinkingConfig payload only when the caller actually supplied\n // one. Our local `thinkingLevel` is typed as `keyof typeof ThinkingLevel`\n // (string union) so users can pass plain strings; the SDK target is the\n // `ThinkingLevel` enum, and every field is `field?: T` under EOPT — so we\n // re-emit fields via conditional spreads.\n const mappedThinkingConfig = thinkingConfig\n ? {\n ...(thinkingConfig.includeThoughts !== undefined && {\n includeThoughts: thinkingConfig.includeThoughts,\n }),\n ...(thinkingConfig.thinkingBudget !== undefined && {\n thinkingBudget: thinkingConfig.thinkingBudget,\n }),\n ...(thinkingConfig.thinkingLevel\n ? {\n thinkingLevel: thinkingConfig.thinkingLevel as ThinkingLevel,\n }\n : {}),\n }\n : undefined\n\n const normalizedPrompts = normalizeSystemPrompts(options.systemPrompts)\n const systemInstruction =\n normalizedPrompts.length > 0\n ? normalizedPrompts.map((p) => p.content).join('\\n')\n : undefined\n\n // Native combined mode (issue #605): when the engine threads\n // `outputSchema` through TextOptions, the adapter declared\n // `supportsCombinedToolsAndSchema` (Gemini 3.x only). The schema is\n // already JSON Schema (pre-converted at the activity boundary). Wire\n // it into `config.responseSchema` + `responseMimeType: 'application/json'`\n // alongside any `tools` — the model emits function calls during the\n // agent loop and the schema-constrained JSON on its natural final\n // turn, so the engine can harvest it without the separate\n // `structuredOutput` finalization round-trip.\n const combinedSchema = options.outputSchema as\n | Record<string, unknown>\n | undefined\n const combinedSchemaConfig = combinedSchema\n ? {\n responseMimeType: 'application/json' as const,\n responseSchema: combinedSchema,\n }\n : undefined\n\n // Vendor `GenerateContentConfig` fields are `field?: T` (no `| undefined`)\n // under EOPT, so spread each common option only when present rather than\n // emitting `field: undefined`s into the wire payload.\n const requestOptions: GenerateContentParameters = {\n model: options.model,\n contents: this.formatMessages(options.messages),\n config: {\n ...modelOpts,\n ...(mappedThinkingConfig !== undefined && {\n thinkingConfig: mappedThinkingConfig,\n }),\n ...(systemInstruction !== undefined && { systemInstruction }),\n tools: convertToolsToProviderFormat(options.tools),\n ...(combinedSchemaConfig ?? {}),\n // Forward the caller's abort signal so cancellation reaches the SDK\n // request, matching the OpenAI-compatible adapters (issue #1374).\n ...(options.request?.signal != null && {\n abortSignal: options.request.signal,\n }),\n },\n }\n\n return requestOptions\n }\n\n /**\n * Gemini 3.x natively combines `tools` + `responseSchema` in a single\n * streaming `generateContentStream` call (issue #605). Gemini 2.x is\n * documented as brittle for the combination and keeps the engine's\n * legacy finalization path.\n */\n supportsCombinedToolsAndSchema(): boolean {\n return GEMINI_COMBINED_TOOLS_AND_SCHEMA_MODELS.has(this.model)\n }\n}\n\nfunction jsonContentParseError(rawText: string, label: string) {\n const snippet = rawText.slice(0, 200)\n const ellipsis = rawText.length > 200 ? '...' : ''\n return `Failed to parse ${label} as JSON. Content: ${snippet}${ellipsis}`\n}\n\nfunction structuredStreamError(\n options: TextOptions<GeminiTextProviderOptions>,\n message: string,\n code: string,\n): Extract<AdapterYieldChunk, { type: typeof EventType.RUN_ERROR }> {\n return {\n type: EventType.RUN_ERROR,\n runId: options.runId,\n model: options.model,\n timestamp: Date.now(),\n message,\n code,\n error: { message, code },\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"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseTTSAdapter } from '@tanstack/ai/adapters';
|
|
2
2
|
import { GEMINI_TTS_MODELS, GeminiTTSVoice } from '../model-meta.js';
|
|
3
|
-
import { TTSOptions, TTSResult } from '@tanstack/ai';
|
|
3
|
+
import { TTSCapabilities, TTSOptions, TTSResult } from '@tanstack/ai';
|
|
4
4
|
import { GeminiClientConfig } from '../utils/client.js';
|
|
5
5
|
/**
|
|
6
6
|
* Configuration for a single speaker in a multi-speaker dialogue.
|
|
@@ -104,6 +104,10 @@ export type GeminiTTSModel = (typeof GEMINI_TTS_MODELS)[number];
|
|
|
104
104
|
*/
|
|
105
105
|
export declare class GeminiTTSAdapter<TModel extends GeminiTTSModel> extends BaseTTSAdapter<TModel, GeminiTTSProviderOptions> {
|
|
106
106
|
readonly name: "gemini";
|
|
107
|
+
/**
|
|
108
|
+
* Gemini multi-speaker TTS tops out at 2 voices, and reports no timings.
|
|
109
|
+
*/
|
|
110
|
+
readonly capabilities: TTSCapabilities;
|
|
107
111
|
private readonly client;
|
|
108
112
|
constructor(config: GeminiTTSConfig, model: TModel);
|
|
109
113
|
/**
|
package/dist/esm/adapters/tts.js
CHANGED
|
@@ -20,6 +20,10 @@ import { BaseTTSAdapter } from "@tanstack/ai/adapters";
|
|
|
20
20
|
*/
|
|
21
21
|
var GeminiTTSAdapter = class extends BaseTTSAdapter {
|
|
22
22
|
name = "gemini";
|
|
23
|
+
/**
|
|
24
|
+
* Gemini multi-speaker TTS tops out at 2 voices, and reports no timings.
|
|
25
|
+
*/
|
|
26
|
+
capabilities = { maxSpeakers: 2 };
|
|
23
27
|
client;
|
|
24
28
|
constructor(config, model) {
|
|
25
29
|
super(model, config);
|
|
@@ -32,19 +36,23 @@ var GeminiTTSAdapter = class extends BaseTTSAdapter {
|
|
|
32
36
|
* @see https://ai.google.dev/gemini-api/docs/speech-generation
|
|
33
37
|
*/
|
|
34
38
|
async generateSpeech(options) {
|
|
35
|
-
const { model,
|
|
39
|
+
const { model, modelOptions, voice, logger, turns } = options;
|
|
36
40
|
logger.request(`activity=generateSpeech provider=gemini model=${model}`, {
|
|
37
41
|
provider: "gemini",
|
|
38
42
|
model
|
|
39
43
|
});
|
|
40
44
|
const speechConfig = {};
|
|
41
|
-
|
|
45
|
+
const text = turns ? turns.map((turn) => `${turn.voice}: ${turn.text}`).join("\n") : options.text;
|
|
46
|
+
if (turns) speechConfig.multiSpeakerVoiceConfig = { speakerVoiceConfigs: [...new Set(turns.map((turn) => turn.voice))].map(toGeminiVoice).map((voiceName) => ({
|
|
47
|
+
speaker: voiceName,
|
|
48
|
+
voiceConfig: { prebuiltVoiceConfig: { voiceName } }
|
|
49
|
+
})) };
|
|
50
|
+
else if (modelOptions?.multiSpeakerVoiceConfig) {
|
|
42
51
|
const speakerConfigs = modelOptions.multiSpeakerVoiceConfig.speakerVoiceConfigs;
|
|
43
52
|
if (!Array.isArray(speakerConfigs) || speakerConfigs.length < 1 || speakerConfigs.length > 2) throw new Error(`Gemini TTS multiSpeakerVoiceConfig.speakerVoiceConfigs must contain 1 or 2 speakers; received ${Array.isArray(speakerConfigs) ? speakerConfigs.length : "non-array"}.`);
|
|
44
53
|
speechConfig.multiSpeakerVoiceConfig = modelOptions.multiSpeakerVoiceConfig;
|
|
45
54
|
} else {
|
|
46
|
-
|
|
47
|
-
const defaultVoiceName = voice ?? "Kore";
|
|
55
|
+
const defaultVoiceName = voice !== void 0 ? toGeminiVoice(voice) : "Kore";
|
|
48
56
|
speechConfig.voiceConfig = { prebuiltVoiceConfig: { voiceName: (modelOptions?.voiceConfig)?.prebuiltVoiceConfig?.voiceName ?? defaultVoiceName } };
|
|
49
57
|
}
|
|
50
58
|
if (modelOptions?.languageCode) speechConfig.languageCode = modelOptions.languageCode;
|
|
@@ -98,6 +106,12 @@ var GeminiTTSAdapter = class extends BaseTTSAdapter {
|
|
|
98
106
|
}
|
|
99
107
|
}
|
|
100
108
|
};
|
|
109
|
+
/** Narrow a caller-supplied voice string to a known Gemini voice, or throw. */
|
|
110
|
+
function toGeminiVoice(voice) {
|
|
111
|
+
const match = GEMINI_TTS_VOICES.find((known) => known === voice);
|
|
112
|
+
if (!match) throw new Error(`Invalid Gemini TTS voice "${voice}". Valid voices are: ${GEMINI_TTS_VOICES.join(", ")}.`);
|
|
113
|
+
return match;
|
|
114
|
+
}
|
|
101
115
|
function parsePcmMimeType(mimeType) {
|
|
102
116
|
const normalized = mimeType.toLowerCase();
|
|
103
117
|
if (((normalized.split(";")[0] ?? "").split("/")[1] ?? "").includes("wav")) return void 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tts.js","names":[],"sources":["../../../src/adapters/tts.ts"],"sourcesContent":["import { BaseTTSAdapter } from '@tanstack/ai/adapters'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport { GEMINI_TTS_VOICES } from '../model-meta'\nimport { buildGeminiUsage } from '../usage'\nimport type { GEMINI_TTS_MODELS, GeminiTTSVoice } from '../model-meta'\nimport type { TTSOptions, TTSResult } from '@tanstack/ai'\nimport type { GoogleGenAI, SpeechConfig } from '@google/genai'\nimport type { GeminiClientConfig } from '../utils/client'\n\n/**\n * Configuration for a single speaker in a multi-speaker dialogue.\n * Supported by Gemini 3.1 Flash TTS Preview and the 2.5 TTS models.\n */\nexport interface GeminiSpeakerVoiceConfig {\n /** A name used in the prompt to refer to this speaker */\n speaker: string\n /** Voice configuration for this speaker */\n voiceConfig: {\n prebuiltVoiceConfig: {\n voiceName: GeminiTTSVoice\n }\n }\n}\n\n/**\n * Provider-specific options for Gemini TTS\n *\n * @experimental Gemini TTS is an experimental feature.\n * @see https://ai.google.dev/gemini-api/docs/speech-generation\n * @see https://ai.google.dev/gemini-api/docs/models/gemini-3.1-flash-tts-preview\n */\nexport interface GeminiTTSProviderOptions {\n /**\n * Voice configuration for single-speaker TTS.\n * Choose from 30 available voices with different characteristics.\n *\n * Use `multiSpeakerVoiceConfig` instead for dialogues.\n */\n voiceConfig?: {\n prebuiltVoiceConfig?: {\n /**\n * The voice name to use for speech synthesis.\n * @see https://ai.google.dev/gemini-api/docs/speech-generation#voices\n */\n voiceName?: GeminiTTSVoice\n }\n }\n\n /**\n * Multi-speaker voice configuration (up to 2 speakers).\n * Supported by Gemini 3.1 Flash TTS Preview and the 2.5 TTS models.\n *\n * Each speaker's lines in the prompt are prefixed with the name defined\n * here, e.g.:\n *\n * ```text\n * Joe: Hey, how's it going?\n * Jane: Not bad, you?\n * ```\n */\n multiSpeakerVoiceConfig?: {\n speakerVoiceConfigs: Array<GeminiSpeakerVoiceConfig>\n }\n\n /**\n * System instruction for controlling speech style.\n * Use natural language to describe the desired speaking style,\n * pace, tone, accent, or other characteristics.\n *\n * With Gemini 3.1 Flash TTS, you can also use inline audio tags like\n * `[whispering]`, `[laughs]`, `[excited]` directly in the input text\n * to control delivery.\n *\n * @example \"Speak slowly and calmly, as if telling a bedtime story\"\n * @example \"Use an upbeat, enthusiastic tone with moderate pace\"\n * @example \"Speak with a British accent\"\n */\n systemInstruction?: string\n\n /**\n * Language code hint for the speech synthesis.\n * Gemini 3.1 Flash TTS supports 70+ languages with auto-detection;\n * the 2.5 TTS models support 24 languages.\n *\n * @example \"en-US\" for American English\n * @example \"es-ES\" for Spanish (Spain)\n * @example \"ja-JP\" for Japanese\n */\n languageCode?: string\n}\n\n/**\n * Configuration for Gemini TTS adapter\n *\n * @experimental Gemini TTS is an experimental feature.\n */\nexport interface GeminiTTSConfig extends GeminiClientConfig {}\n\n/** Model type for Gemini TTS */\nexport type GeminiTTSModel = (typeof GEMINI_TTS_MODELS)[number]\n\n/**\n * Gemini Text-to-Speech Adapter\n *\n * Tree-shakeable adapter for Gemini TTS functionality.\n *\n * **IMPORTANT**: Gemini TTS uses the Live API (WebSocket-based) which requires\n * different handling than traditional REST APIs. This adapter provides a\n * simplified interface but may have limitations.\n *\n * @experimental Gemini TTS is an experimental feature and may change.\n *\n * Models:\n * - gemini-2.5-flash-preview-tts\n */\nexport class GeminiTTSAdapter<\n TModel extends GeminiTTSModel,\n> extends BaseTTSAdapter<TModel, GeminiTTSProviderOptions> {\n readonly name = 'gemini' as const\n\n private readonly client: GoogleGenAI\n\n constructor(config: GeminiTTSConfig, model: TModel) {\n super(model, config)\n this.client = createGeminiClient(config)\n }\n\n /**\n * Generate speech from text using Gemini's TTS model.\n *\n * @experimental This implementation is experimental and may change.\n * @see https://ai.google.dev/gemini-api/docs/speech-generation\n */\n async generateSpeech(\n options: TTSOptions<GeminiTTSProviderOptions>,\n ): Promise<TTSResult> {\n const { model, text, modelOptions, voice, logger } = options\n\n logger.request(`activity=generateSpeech provider=gemini model=${model}`, {\n provider: 'gemini',\n model,\n })\n\n const speechConfig: SpeechConfig = {}\n\n if (modelOptions?.multiSpeakerVoiceConfig) {\n // Validate multi-speaker config: 1 or 2 speakers allowed.\n const speakerConfigs =\n modelOptions.multiSpeakerVoiceConfig.speakerVoiceConfigs\n if (\n !Array.isArray(speakerConfigs) ||\n speakerConfigs.length < 1 ||\n speakerConfigs.length > 2\n ) {\n throw new Error(\n `Gemini TTS multiSpeakerVoiceConfig.speakerVoiceConfigs must contain 1 or 2 speakers; received ${Array.isArray(speakerConfigs) ? speakerConfigs.length : 'non-array'}.`,\n )\n }\n speechConfig.multiSpeakerVoiceConfig =\n modelOptions.multiSpeakerVoiceConfig\n } else {\n // Honor the standard TTSOptions.voice (used by every other TTS adapter)\n // as a fallback for the prebuilt voice name. If an explicit\n // modelOptions.voiceConfig is supplied its values win — but we still\n // fall back to `voice` / 'Kore' if the supplied voiceConfig is missing\n // prebuiltVoiceConfig.voiceName.\n if (\n voice !== undefined &&\n !(GEMINI_TTS_VOICES as ReadonlyArray<string>).includes(voice)\n ) {\n throw new Error(\n `Invalid Gemini TTS voice \"${voice}\". Valid voices are: ${GEMINI_TTS_VOICES.join(', ')}.`,\n )\n }\n const defaultVoiceName = (voice as GeminiTTSVoice | undefined) ?? 'Kore'\n const supplied = modelOptions?.voiceConfig\n const resolvedVoiceName =\n supplied?.prebuiltVoiceConfig?.voiceName ?? defaultVoiceName\n speechConfig.voiceConfig = {\n prebuiltVoiceConfig: { voiceName: resolvedVoiceName },\n }\n }\n\n if (modelOptions?.languageCode) {\n speechConfig.languageCode = modelOptions.languageCode\n }\n\n try {\n const response = await this.client.models.generateContent({\n model,\n contents: [\n {\n role: 'user',\n parts: [{ text }],\n },\n ],\n config: {\n responseModalities: ['AUDIO'],\n speechConfig,\n // systemInstruction belongs inside `config` per the @google/genai\n // contract — matches sibling Gemini adapters (summarize, text).\n ...(modelOptions?.systemInstruction && {\n systemInstruction: modelOptions.systemInstruction,\n }),\n },\n })\n\n // Extract audio data from response\n const candidate = response.candidates?.[0]\n const parts = candidate?.content?.parts\n\n if (!parts || parts.length === 0) {\n throw new Error('No audio output received from Gemini TTS')\n }\n\n // Look for inline data (audio)\n const audioPart = parts.find((part: any) =>\n part.inlineData?.mimeType?.startsWith('audio/'),\n )\n\n if (!audioPart || !audioPart.inlineData || !audioPart.inlineData.data) {\n throw new Error('No audio data in Gemini TTS response')\n }\n\n const audioBase64 = audioPart.inlineData.data\n // mime is guaranteed by the `startsWith('audio/')` find predicate above.\n const mimeType = audioPart.inlineData.mimeType as string\n\n // Surface token usage (with per-modality breakdown) when Gemini reports\n // it. Spread conditionally for exactOptionalPropertyTypes — shared by both\n // the PCM→WAV and pass-through return paths below.\n const usageField = response.usageMetadata\n ? { usage: buildGeminiUsage(response.usageMetadata) }\n : {}\n\n // Gemini TTS models return raw 16-bit LE PCM with a mime type like\n // `audio/L16;codec=pcm;rate=24000`. That isn't playable in an <audio>\n // element and the bare string isn't a usable file extension, so we\n // prepend a RIFF/WAV header here and normalize the result to audio/wav.\n const pcm = parsePcmMimeType(mimeType)\n if (pcm) {\n const wavBase64 = wrapPcmBase64AsWav(\n audioBase64,\n pcm.sampleRate,\n pcm.channels,\n pcm.bitsPerSample,\n )\n return {\n id: generateId(this.name),\n model,\n audio: wavBase64,\n format: 'wav',\n contentType: 'audio/wav',\n ...usageField,\n }\n }\n\n // Strip any mime parameters (e.g. `audio/ogg;codec=opus`) before pulling\n // the subtype out as the file format. `String.split` always returns at\n // least one element, so [0] is defined.\n const format = (mimeType.split(';')[0] ?? '').split('/')[1] || 'wav'\n\n return {\n id: generateId(this.name),\n model,\n audio: audioBase64,\n format,\n contentType: mimeType,\n ...usageField,\n }\n } catch (error) {\n logger.errors('gemini.generateSpeech fatal', {\n error,\n source: 'gemini.generateSpeech',\n })\n throw error\n }\n }\n}\n\nfunction parsePcmMimeType(\n mimeType: string,\n): { sampleRate: number; channels: number; bitsPerSample: number } | undefined {\n const normalized = mimeType.toLowerCase()\n const subtype = (normalized.split(';')[0] ?? '').split('/')[1] ?? ''\n // Exclude containerized wav (e.g. `audio/wav;codec=pcm`) — those already\n // carry a RIFF header and must not be re-wrapped.\n if (subtype.includes('wav')) return undefined\n\n // Accept the variants Gemini and other providers actually emit:\n // - audio/L16;codec=pcm;rate=24000 (IANA PCM with bit depth in the type)\n // - audio/L24 and friends\n // - audio/pcm and audio/x-pcm\n // - anything else that explicitly tags codec=pcm and isn't wav-containered\n const bitDepthMatch = /^audio\\/l(\\d+)/.exec(normalized)\n const isPcm =\n bitDepthMatch !== null ||\n normalized.startsWith('audio/pcm') ||\n normalized.startsWith('audio/x-pcm') ||\n normalized.includes('codec=pcm')\n if (!isPcm) return undefined\n\n const rateMatch = /rate=(\\d+)/.exec(normalized)\n const channelsMatch = /channels=(\\d+)/.exec(normalized)\n // Default to 16-bit when the mime type doesn't specify — matches Gemini's\n // audio/L16;codec=pcm;rate=24000 response.\n const bitsPerSample = bitDepthMatch ? Number(bitDepthMatch[1]) : 16\n return {\n sampleRate: rateMatch ? Number(rateMatch[1]) : 24000,\n channels: channelsMatch ? Number(channelsMatch[1]) : 1,\n bitsPerSample,\n }\n}\n\nfunction wrapPcmBase64AsWav(\n pcmBase64: string,\n sampleRate: number,\n channels = 1,\n bitsPerSample = 16,\n): string {\n // The WAV writer below emits a 16-bit PCM fmt chunk. If the source claims a\n // different bit depth we'd be lying about the payload, so bail out loudly\n // rather than producing a corrupt file.\n if (bitsPerSample !== 16) {\n throw new Error(\n `Unsupported PCM bit depth ${bitsPerSample}: only 16-bit PCM can be wrapped as WAV.`,\n )\n }\n\n const pcmBytes =\n typeof Buffer !== 'undefined'\n ? new Uint8Array(Buffer.from(pcmBase64, 'base64'))\n : decodeBase64(pcmBase64)\n\n const byteRate = (sampleRate * channels * bitsPerSample) / 8\n const blockAlign = (channels * bitsPerSample) / 8\n const dataSize = pcmBytes.byteLength\n const buffer = new ArrayBuffer(44 + dataSize)\n const view = new DataView(buffer)\n\n writeAscii(view, 0, 'RIFF')\n view.setUint32(4, 36 + dataSize, true)\n writeAscii(view, 8, 'WAVE')\n writeAscii(view, 12, 'fmt ')\n view.setUint32(16, 16, true)\n view.setUint16(20, 1, true)\n view.setUint16(22, channels, true)\n view.setUint32(24, sampleRate, true)\n view.setUint32(28, byteRate, true)\n view.setUint16(32, blockAlign, true)\n view.setUint16(34, bitsPerSample, true)\n writeAscii(view, 36, 'data')\n view.setUint32(40, dataSize, true)\n new Uint8Array(buffer, 44).set(pcmBytes)\n\n if (typeof Buffer !== 'undefined') {\n return Buffer.from(buffer).toString('base64')\n }\n let binary = ''\n const bytes = new Uint8Array(buffer)\n for (const byte of bytes) {\n binary += String.fromCharCode(byte)\n }\n return btoa(binary)\n}\n\nfunction decodeBase64(b64: string): Uint8Array {\n const binary = atob(b64)\n const out = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i)\n return out\n}\n\nfunction writeAscii(view: DataView, offset: number, text: string): void {\n for (let i = 0; i < text.length; i += 1) {\n view.setUint8(offset + i, text.charCodeAt(i))\n }\n}\n\n/**\n * Creates a Gemini TTS adapter with explicit API key.\n * Type resolution happens here at the call site.\n *\n * @experimental Gemini TTS is an experimental feature and may change.\n *\n * @param model - The model name (e.g., 'gemini-2.5-flash-preview-tts')\n * @param apiKey - Your Google API key\n * @param config - Optional additional configuration\n * @returns Configured Gemini TTS adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createGeminiSpeech('gemini-2.5-flash-preview-tts', \"your-api-key\");\n *\n * const result = await generateSpeech({\n * adapter,\n * text: 'Hello, world!'\n * });\n * ```\n */\nexport function createGeminiSpeech<TModel extends GeminiTTSModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiTTSConfig, 'apiKey'>,\n): GeminiTTSAdapter<TModel> {\n // Put apiKey LAST so caller-supplied config can't silently override the\n // explicit argument.\n return new GeminiTTSAdapter({ ...config, apiKey }, model)\n}\n\n/**\n * Creates a Gemini speech adapter with automatic API key detection from environment variables.\n * Type resolution happens here at the call site.\n *\n * @experimental Gemini TTS is an experimental feature and may change.\n *\n * Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in:\n * - `process.env` (Node.js)\n * - `window.env` (Browser with injected env)\n *\n * @param model - The model name (e.g., 'gemini-2.5-flash-preview-tts')\n * @param config - Optional configuration (excluding apiKey which is auto-detected)\n * @returns Configured Gemini speech adapter instance with resolved types\n * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * // Automatically uses GOOGLE_API_KEY from environment\n * const adapter = geminiSpeech('gemini-2.5-flash-preview-tts');\n *\n * const result = await generateSpeech({\n * adapter,\n * text: 'Welcome to TanStack AI!'\n * });\n * ```\n */\nexport function geminiSpeech<TModel extends GeminiTTSModel>(\n model: TModel,\n config?: Omit<GeminiTTSConfig, 'apiKey'>,\n): GeminiTTSAdapter<TModel> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiSpeech(model, apiKey, config)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tts.js","names":[],"sources":["../../../src/adapters/tts.ts"],"sourcesContent":["import { BaseTTSAdapter } from '@tanstack/ai/adapters'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport { GEMINI_TTS_VOICES } from '../model-meta'\nimport { buildGeminiUsage } from '../usage'\nimport type { GEMINI_TTS_MODELS, GeminiTTSVoice } from '../model-meta'\nimport type { TTSCapabilities, TTSOptions, TTSResult } from '@tanstack/ai'\nimport type { GoogleGenAI, SpeechConfig } from '@google/genai'\nimport type { GeminiClientConfig } from '../utils/client'\n\n/**\n * Configuration for a single speaker in a multi-speaker dialogue.\n * Supported by Gemini 3.1 Flash TTS Preview and the 2.5 TTS models.\n */\nexport interface GeminiSpeakerVoiceConfig {\n /** A name used in the prompt to refer to this speaker */\n speaker: string\n /** Voice configuration for this speaker */\n voiceConfig: {\n prebuiltVoiceConfig: {\n voiceName: GeminiTTSVoice\n }\n }\n}\n\n/**\n * Provider-specific options for Gemini TTS\n *\n * @experimental Gemini TTS is an experimental feature.\n * @see https://ai.google.dev/gemini-api/docs/speech-generation\n * @see https://ai.google.dev/gemini-api/docs/models/gemini-3.1-flash-tts-preview\n */\nexport interface GeminiTTSProviderOptions {\n /**\n * Voice configuration for single-speaker TTS.\n * Choose from 30 available voices with different characteristics.\n *\n * Use `multiSpeakerVoiceConfig` instead for dialogues.\n */\n voiceConfig?: {\n prebuiltVoiceConfig?: {\n /**\n * The voice name to use for speech synthesis.\n * @see https://ai.google.dev/gemini-api/docs/speech-generation#voices\n */\n voiceName?: GeminiTTSVoice\n }\n }\n\n /**\n * Multi-speaker voice configuration (up to 2 speakers).\n * Supported by Gemini 3.1 Flash TTS Preview and the 2.5 TTS models.\n *\n * Each speaker's lines in the prompt are prefixed with the name defined\n * here, e.g.:\n *\n * ```text\n * Joe: Hey, how's it going?\n * Jane: Not bad, you?\n * ```\n */\n multiSpeakerVoiceConfig?: {\n speakerVoiceConfigs: Array<GeminiSpeakerVoiceConfig>\n }\n\n /**\n * System instruction for controlling speech style.\n * Use natural language to describe the desired speaking style,\n * pace, tone, accent, or other characteristics.\n *\n * With Gemini 3.1 Flash TTS, you can also use inline audio tags like\n * `[whispering]`, `[laughs]`, `[excited]` directly in the input text\n * to control delivery.\n *\n * @example \"Speak slowly and calmly, as if telling a bedtime story\"\n * @example \"Use an upbeat, enthusiastic tone with moderate pace\"\n * @example \"Speak with a British accent\"\n */\n systemInstruction?: string\n\n /**\n * Language code hint for the speech synthesis.\n * Gemini 3.1 Flash TTS supports 70+ languages with auto-detection;\n * the 2.5 TTS models support 24 languages.\n *\n * @example \"en-US\" for American English\n * @example \"es-ES\" for Spanish (Spain)\n * @example \"ja-JP\" for Japanese\n */\n languageCode?: string\n}\n\n/**\n * Configuration for Gemini TTS adapter\n *\n * @experimental Gemini TTS is an experimental feature.\n */\nexport interface GeminiTTSConfig extends GeminiClientConfig {}\n\n/** Model type for Gemini TTS */\nexport type GeminiTTSModel = (typeof GEMINI_TTS_MODELS)[number]\n\n/**\n * Gemini Text-to-Speech Adapter\n *\n * Tree-shakeable adapter for Gemini TTS functionality.\n *\n * **IMPORTANT**: Gemini TTS uses the Live API (WebSocket-based) which requires\n * different handling than traditional REST APIs. This adapter provides a\n * simplified interface but may have limitations.\n *\n * @experimental Gemini TTS is an experimental feature and may change.\n *\n * Models:\n * - gemini-2.5-flash-preview-tts\n */\nexport class GeminiTTSAdapter<\n TModel extends GeminiTTSModel,\n> extends BaseTTSAdapter<TModel, GeminiTTSProviderOptions> {\n readonly name = 'gemini' as const\n\n /**\n * Gemini multi-speaker TTS tops out at 2 voices, and reports no timings.\n */\n override readonly capabilities: TTSCapabilities = { maxSpeakers: 2 }\n\n private readonly client: GoogleGenAI\n\n constructor(config: GeminiTTSConfig, model: TModel) {\n super(model, config)\n this.client = createGeminiClient(config)\n }\n\n /**\n * Generate speech from text using Gemini's TTS model.\n *\n * @experimental This implementation is experimental and may change.\n * @see https://ai.google.dev/gemini-api/docs/speech-generation\n */\n async generateSpeech(\n options: TTSOptions<GeminiTTSProviderOptions>,\n ): Promise<TTSResult> {\n const { model, modelOptions, voice, logger, turns } = options\n\n logger.request(`activity=generateSpeech provider=gemini model=${model}`, {\n provider: 'gemini',\n model,\n })\n\n const speechConfig: SpeechConfig = {}\n // Gemini has no turn structure on the wire: speakers are named in the\n // prompt and mapped to voices in the config. `turns` carries voice names,\n // so the voice name doubles as the speaker label.\n const text = turns\n ? turns.map((turn) => `${turn.voice}: ${turn.text}`).join('\\n')\n : options.text\n\n if (turns) {\n const voiceNames = [...new Set(turns.map((turn) => turn.voice))].map(\n toGeminiVoice,\n )\n speechConfig.multiSpeakerVoiceConfig = {\n speakerVoiceConfigs: voiceNames.map((voiceName) => ({\n speaker: voiceName,\n voiceConfig: { prebuiltVoiceConfig: { voiceName } },\n })),\n }\n } else if (modelOptions?.multiSpeakerVoiceConfig) {\n // Validate multi-speaker config: 1 or 2 speakers allowed.\n const speakerConfigs =\n modelOptions.multiSpeakerVoiceConfig.speakerVoiceConfigs\n if (\n !Array.isArray(speakerConfigs) ||\n speakerConfigs.length < 1 ||\n speakerConfigs.length > 2\n ) {\n throw new Error(\n `Gemini TTS multiSpeakerVoiceConfig.speakerVoiceConfigs must contain 1 or 2 speakers; received ${Array.isArray(speakerConfigs) ? speakerConfigs.length : 'non-array'}.`,\n )\n }\n speechConfig.multiSpeakerVoiceConfig =\n modelOptions.multiSpeakerVoiceConfig\n } else {\n // Honor the standard TTSOptions.voice (used by every other TTS adapter)\n // as a fallback for the prebuilt voice name. If an explicit\n // modelOptions.voiceConfig is supplied its values win — but we still\n // fall back to `voice` / 'Kore' if the supplied voiceConfig is missing\n // prebuiltVoiceConfig.voiceName.\n const defaultVoiceName =\n voice !== undefined ? toGeminiVoice(voice) : 'Kore'\n const supplied = modelOptions?.voiceConfig\n const resolvedVoiceName =\n supplied?.prebuiltVoiceConfig?.voiceName ?? defaultVoiceName\n speechConfig.voiceConfig = {\n prebuiltVoiceConfig: { voiceName: resolvedVoiceName },\n }\n }\n\n if (modelOptions?.languageCode) {\n speechConfig.languageCode = modelOptions.languageCode\n }\n\n try {\n const response = await this.client.models.generateContent({\n model,\n contents: [\n {\n role: 'user',\n parts: [{ text }],\n },\n ],\n config: {\n responseModalities: ['AUDIO'],\n speechConfig,\n // systemInstruction belongs inside `config` per the @google/genai\n // contract — matches sibling Gemini adapters (summarize, text).\n ...(modelOptions?.systemInstruction && {\n systemInstruction: modelOptions.systemInstruction,\n }),\n },\n })\n\n // Extract audio data from response\n const candidate = response.candidates?.[0]\n const parts = candidate?.content?.parts\n\n if (!parts || parts.length === 0) {\n throw new Error('No audio output received from Gemini TTS')\n }\n\n // Look for inline data (audio)\n const audioPart = parts.find((part: any) =>\n part.inlineData?.mimeType?.startsWith('audio/'),\n )\n\n if (!audioPart || !audioPart.inlineData || !audioPart.inlineData.data) {\n throw new Error('No audio data in Gemini TTS response')\n }\n\n const audioBase64 = audioPart.inlineData.data\n // mime is guaranteed by the `startsWith('audio/')` find predicate above.\n const mimeType = audioPart.inlineData.mimeType as string\n\n // Surface token usage (with per-modality breakdown) when Gemini reports\n // it. Spread conditionally for exactOptionalPropertyTypes — shared by both\n // the PCM→WAV and pass-through return paths below.\n const usageField = response.usageMetadata\n ? { usage: buildGeminiUsage(response.usageMetadata) }\n : {}\n\n // Gemini TTS models return raw 16-bit LE PCM with a mime type like\n // `audio/L16;codec=pcm;rate=24000`. That isn't playable in an <audio>\n // element and the bare string isn't a usable file extension, so we\n // prepend a RIFF/WAV header here and normalize the result to audio/wav.\n const pcm = parsePcmMimeType(mimeType)\n if (pcm) {\n const wavBase64 = wrapPcmBase64AsWav(\n audioBase64,\n pcm.sampleRate,\n pcm.channels,\n pcm.bitsPerSample,\n )\n return {\n id: generateId(this.name),\n model,\n audio: wavBase64,\n format: 'wav',\n contentType: 'audio/wav',\n ...usageField,\n }\n }\n\n // Strip any mime parameters (e.g. `audio/ogg;codec=opus`) before pulling\n // the subtype out as the file format. `String.split` always returns at\n // least one element, so [0] is defined.\n const format = (mimeType.split(';')[0] ?? '').split('/')[1] || 'wav'\n\n return {\n id: generateId(this.name),\n model,\n audio: audioBase64,\n format,\n contentType: mimeType,\n ...usageField,\n }\n } catch (error) {\n logger.errors('gemini.generateSpeech fatal', {\n error,\n source: 'gemini.generateSpeech',\n })\n throw error\n }\n }\n}\n\n/** Narrow a caller-supplied voice string to a known Gemini voice, or throw. */\nfunction toGeminiVoice(voice: string): GeminiTTSVoice {\n const match = GEMINI_TTS_VOICES.find((known) => known === voice)\n if (!match) {\n throw new Error(\n `Invalid Gemini TTS voice \"${voice}\". Valid voices are: ${GEMINI_TTS_VOICES.join(', ')}.`,\n )\n }\n return match\n}\n\nfunction parsePcmMimeType(\n mimeType: string,\n): { sampleRate: number; channels: number; bitsPerSample: number } | undefined {\n const normalized = mimeType.toLowerCase()\n const subtype = (normalized.split(';')[0] ?? '').split('/')[1] ?? ''\n // Exclude containerized wav (e.g. `audio/wav;codec=pcm`) — those already\n // carry a RIFF header and must not be re-wrapped.\n if (subtype.includes('wav')) return undefined\n\n // Accept the variants Gemini and other providers actually emit:\n // - audio/L16;codec=pcm;rate=24000 (IANA PCM with bit depth in the type)\n // - audio/L24 and friends\n // - audio/pcm and audio/x-pcm\n // - anything else that explicitly tags codec=pcm and isn't wav-containered\n const bitDepthMatch = /^audio\\/l(\\d+)/.exec(normalized)\n const isPcm =\n bitDepthMatch !== null ||\n normalized.startsWith('audio/pcm') ||\n normalized.startsWith('audio/x-pcm') ||\n normalized.includes('codec=pcm')\n if (!isPcm) return undefined\n\n const rateMatch = /rate=(\\d+)/.exec(normalized)\n const channelsMatch = /channels=(\\d+)/.exec(normalized)\n // Default to 16-bit when the mime type doesn't specify — matches Gemini's\n // audio/L16;codec=pcm;rate=24000 response.\n const bitsPerSample = bitDepthMatch ? Number(bitDepthMatch[1]) : 16\n return {\n sampleRate: rateMatch ? Number(rateMatch[1]) : 24000,\n channels: channelsMatch ? Number(channelsMatch[1]) : 1,\n bitsPerSample,\n }\n}\n\nfunction wrapPcmBase64AsWav(\n pcmBase64: string,\n sampleRate: number,\n channels = 1,\n bitsPerSample = 16,\n): string {\n // The WAV writer below emits a 16-bit PCM fmt chunk. If the source claims a\n // different bit depth we'd be lying about the payload, so bail out loudly\n // rather than producing a corrupt file.\n if (bitsPerSample !== 16) {\n throw new Error(\n `Unsupported PCM bit depth ${bitsPerSample}: only 16-bit PCM can be wrapped as WAV.`,\n )\n }\n\n const pcmBytes =\n typeof Buffer !== 'undefined'\n ? new Uint8Array(Buffer.from(pcmBase64, 'base64'))\n : decodeBase64(pcmBase64)\n\n const byteRate = (sampleRate * channels * bitsPerSample) / 8\n const blockAlign = (channels * bitsPerSample) / 8\n const dataSize = pcmBytes.byteLength\n const buffer = new ArrayBuffer(44 + dataSize)\n const view = new DataView(buffer)\n\n writeAscii(view, 0, 'RIFF')\n view.setUint32(4, 36 + dataSize, true)\n writeAscii(view, 8, 'WAVE')\n writeAscii(view, 12, 'fmt ')\n view.setUint32(16, 16, true)\n view.setUint16(20, 1, true)\n view.setUint16(22, channels, true)\n view.setUint32(24, sampleRate, true)\n view.setUint32(28, byteRate, true)\n view.setUint16(32, blockAlign, true)\n view.setUint16(34, bitsPerSample, true)\n writeAscii(view, 36, 'data')\n view.setUint32(40, dataSize, true)\n new Uint8Array(buffer, 44).set(pcmBytes)\n\n if (typeof Buffer !== 'undefined') {\n return Buffer.from(buffer).toString('base64')\n }\n let binary = ''\n const bytes = new Uint8Array(buffer)\n for (const byte of bytes) {\n binary += String.fromCharCode(byte)\n }\n return btoa(binary)\n}\n\nfunction decodeBase64(b64: string): Uint8Array {\n const binary = atob(b64)\n const out = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i)\n return out\n}\n\nfunction writeAscii(view: DataView, offset: number, text: string): void {\n for (let i = 0; i < text.length; i += 1) {\n view.setUint8(offset + i, text.charCodeAt(i))\n }\n}\n\n/**\n * Creates a Gemini TTS adapter with explicit API key.\n * Type resolution happens here at the call site.\n *\n * @experimental Gemini TTS is an experimental feature and may change.\n *\n * @param model - The model name (e.g., 'gemini-2.5-flash-preview-tts')\n * @param apiKey - Your Google API key\n * @param config - Optional additional configuration\n * @returns Configured Gemini TTS adapter instance with resolved types\n *\n * @example\n * ```typescript\n * const adapter = createGeminiSpeech('gemini-2.5-flash-preview-tts', \"your-api-key\");\n *\n * const result = await generateSpeech({\n * adapter,\n * text: 'Hello, world!'\n * });\n * ```\n */\nexport function createGeminiSpeech<TModel extends GeminiTTSModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiTTSConfig, 'apiKey'>,\n): GeminiTTSAdapter<TModel> {\n // Put apiKey LAST so caller-supplied config can't silently override the\n // explicit argument.\n return new GeminiTTSAdapter({ ...config, apiKey }, model)\n}\n\n/**\n * Creates a Gemini speech adapter with automatic API key detection from environment variables.\n * Type resolution happens here at the call site.\n *\n * @experimental Gemini TTS is an experimental feature and may change.\n *\n * Looks for `GOOGLE_API_KEY` or `GEMINI_API_KEY` in:\n * - `process.env` (Node.js)\n * - `window.env` (Browser with injected env)\n *\n * @param model - The model name (e.g., 'gemini-2.5-flash-preview-tts')\n * @param config - Optional configuration (excluding apiKey which is auto-detected)\n * @returns Configured Gemini speech adapter instance with resolved types\n * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found in environment\n *\n * @example\n * ```typescript\n * // Automatically uses GOOGLE_API_KEY from environment\n * const adapter = geminiSpeech('gemini-2.5-flash-preview-tts');\n *\n * const result = await generateSpeech({\n * adapter,\n * text: 'Welcome to TanStack AI!'\n * });\n * ```\n */\nexport function geminiSpeech<TModel extends GeminiTTSModel>(\n model: TModel,\n config?: Omit<GeminiTTSConfig, 'apiKey'>,\n): GeminiTTSAdapter<TModel> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiSpeech(model, apiKey, config)\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-gemini",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.2",
|
|
4
4
|
"description": "Google Gemini adapter for TanStack AI chat, images, speech, audio generation, and structured outputs.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -60,16 +60,16 @@
|
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@google/genai": "^2.10.0",
|
|
62
62
|
"partial-json": "^0.1.7",
|
|
63
|
-
"@tanstack/ai-utils": "^0.4.
|
|
63
|
+
"@tanstack/ai-utils": "^0.4.1"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"@tanstack/ai": "^0.
|
|
66
|
+
"@tanstack/ai": "^0.58.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@vitest/coverage-v8": "4.1.10",
|
|
70
70
|
"vite": "^8.2.1",
|
|
71
71
|
"zod": "^4.2.0",
|
|
72
|
-
"@tanstack/ai": "0.
|
|
72
|
+
"@tanstack/ai": "0.58.0"
|
|
73
73
|
},
|
|
74
74
|
"scripts": {
|
|
75
75
|
"build": "vite build",
|
package/src/adapters/text.ts
CHANGED
|
@@ -1097,7 +1097,7 @@ export class GeminiTextAdapter<
|
|
|
1097
1097
|
* user messages in multi-turn conversations.
|
|
1098
1098
|
*
|
|
1099
1099
|
* Also filters out empty model messages (e.g., from a previous failed request)
|
|
1100
|
-
* and deduplicates functionResponse parts with the same
|
|
1100
|
+
* and deduplicates functionResponse parts with the same id (tool call ID).
|
|
1101
1101
|
*/
|
|
1102
1102
|
private mergeConsecutiveSameRoleMessages(
|
|
1103
1103
|
messages: Array<Content>,
|
|
@@ -1128,16 +1128,20 @@ export class GeminiTextAdapter<
|
|
|
1128
1128
|
}
|
|
1129
1129
|
}
|
|
1130
1130
|
|
|
1131
|
-
// Deduplicate functionResponse parts with the same
|
|
1131
|
+
// Deduplicate functionResponse parts with the same id (tool call ID).
|
|
1132
|
+
// Two parallel calls to the *same* tool share a `name` but have distinct
|
|
1133
|
+
// `id`s — keying on `name` dropped every response but the first for
|
|
1134
|
+
// same-tool parallel calls, leaving Gemini with fewer response parts
|
|
1135
|
+
// than call parts and a 400 on the next request.
|
|
1132
1136
|
for (const msg of merged) {
|
|
1133
1137
|
if (!msg.parts) continue
|
|
1134
|
-
const
|
|
1138
|
+
const seenFunctionResponseIds = new Set<string>()
|
|
1135
1139
|
msg.parts = msg.parts.filter((part) => {
|
|
1136
|
-
if ('functionResponse' in part && part.functionResponse?.
|
|
1137
|
-
if (
|
|
1140
|
+
if ('functionResponse' in part && part.functionResponse?.id) {
|
|
1141
|
+
if (seenFunctionResponseIds.has(part.functionResponse.id)) {
|
|
1138
1142
|
return false
|
|
1139
1143
|
}
|
|
1140
|
-
|
|
1144
|
+
seenFunctionResponseIds.add(part.functionResponse.id)
|
|
1141
1145
|
}
|
|
1142
1146
|
return true
|
|
1143
1147
|
})
|
package/src/adapters/tts.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import { GEMINI_TTS_VOICES } from '../model-meta'
|
|
8
8
|
import { buildGeminiUsage } from '../usage'
|
|
9
9
|
import type { GEMINI_TTS_MODELS, GeminiTTSVoice } from '../model-meta'
|
|
10
|
-
import type { TTSOptions, TTSResult } from '@tanstack/ai'
|
|
10
|
+
import type { TTSCapabilities, TTSOptions, TTSResult } from '@tanstack/ai'
|
|
11
11
|
import type { GoogleGenAI, SpeechConfig } from '@google/genai'
|
|
12
12
|
import type { GeminiClientConfig } from '../utils/client'
|
|
13
13
|
|
|
@@ -122,6 +122,11 @@ export class GeminiTTSAdapter<
|
|
|
122
122
|
> extends BaseTTSAdapter<TModel, GeminiTTSProviderOptions> {
|
|
123
123
|
readonly name = 'gemini' as const
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Gemini multi-speaker TTS tops out at 2 voices, and reports no timings.
|
|
127
|
+
*/
|
|
128
|
+
override readonly capabilities: TTSCapabilities = { maxSpeakers: 2 }
|
|
129
|
+
|
|
125
130
|
private readonly client: GoogleGenAI
|
|
126
131
|
|
|
127
132
|
constructor(config: GeminiTTSConfig, model: TModel) {
|
|
@@ -138,7 +143,7 @@ export class GeminiTTSAdapter<
|
|
|
138
143
|
async generateSpeech(
|
|
139
144
|
options: TTSOptions<GeminiTTSProviderOptions>,
|
|
140
145
|
): Promise<TTSResult> {
|
|
141
|
-
const { model,
|
|
146
|
+
const { model, modelOptions, voice, logger, turns } = options
|
|
142
147
|
|
|
143
148
|
logger.request(`activity=generateSpeech provider=gemini model=${model}`, {
|
|
144
149
|
provider: 'gemini',
|
|
@@ -146,8 +151,24 @@ export class GeminiTTSAdapter<
|
|
|
146
151
|
})
|
|
147
152
|
|
|
148
153
|
const speechConfig: SpeechConfig = {}
|
|
149
|
-
|
|
150
|
-
|
|
154
|
+
// Gemini has no turn structure on the wire: speakers are named in the
|
|
155
|
+
// prompt and mapped to voices in the config. `turns` carries voice names,
|
|
156
|
+
// so the voice name doubles as the speaker label.
|
|
157
|
+
const text = turns
|
|
158
|
+
? turns.map((turn) => `${turn.voice}: ${turn.text}`).join('\n')
|
|
159
|
+
: options.text
|
|
160
|
+
|
|
161
|
+
if (turns) {
|
|
162
|
+
const voiceNames = [...new Set(turns.map((turn) => turn.voice))].map(
|
|
163
|
+
toGeminiVoice,
|
|
164
|
+
)
|
|
165
|
+
speechConfig.multiSpeakerVoiceConfig = {
|
|
166
|
+
speakerVoiceConfigs: voiceNames.map((voiceName) => ({
|
|
167
|
+
speaker: voiceName,
|
|
168
|
+
voiceConfig: { prebuiltVoiceConfig: { voiceName } },
|
|
169
|
+
})),
|
|
170
|
+
}
|
|
171
|
+
} else if (modelOptions?.multiSpeakerVoiceConfig) {
|
|
151
172
|
// Validate multi-speaker config: 1 or 2 speakers allowed.
|
|
152
173
|
const speakerConfigs =
|
|
153
174
|
modelOptions.multiSpeakerVoiceConfig.speakerVoiceConfigs
|
|
@@ -168,15 +189,8 @@ export class GeminiTTSAdapter<
|
|
|
168
189
|
// modelOptions.voiceConfig is supplied its values win — but we still
|
|
169
190
|
// fall back to `voice` / 'Kore' if the supplied voiceConfig is missing
|
|
170
191
|
// prebuiltVoiceConfig.voiceName.
|
|
171
|
-
|
|
172
|
-
voice !== undefined
|
|
173
|
-
!(GEMINI_TTS_VOICES as ReadonlyArray<string>).includes(voice)
|
|
174
|
-
) {
|
|
175
|
-
throw new Error(
|
|
176
|
-
`Invalid Gemini TTS voice "${voice}". Valid voices are: ${GEMINI_TTS_VOICES.join(', ')}.`,
|
|
177
|
-
)
|
|
178
|
-
}
|
|
179
|
-
const defaultVoiceName = (voice as GeminiTTSVoice | undefined) ?? 'Kore'
|
|
192
|
+
const defaultVoiceName =
|
|
193
|
+
voice !== undefined ? toGeminiVoice(voice) : 'Kore'
|
|
180
194
|
const supplied = modelOptions?.voiceConfig
|
|
181
195
|
const resolvedVoiceName =
|
|
182
196
|
supplied?.prebuiltVoiceConfig?.voiceName ?? defaultVoiceName
|
|
@@ -282,6 +296,17 @@ export class GeminiTTSAdapter<
|
|
|
282
296
|
}
|
|
283
297
|
}
|
|
284
298
|
|
|
299
|
+
/** Narrow a caller-supplied voice string to a known Gemini voice, or throw. */
|
|
300
|
+
function toGeminiVoice(voice: string): GeminiTTSVoice {
|
|
301
|
+
const match = GEMINI_TTS_VOICES.find((known) => known === voice)
|
|
302
|
+
if (!match) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`Invalid Gemini TTS voice "${voice}". Valid voices are: ${GEMINI_TTS_VOICES.join(', ')}.`,
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
return match
|
|
308
|
+
}
|
|
309
|
+
|
|
285
310
|
function parsePcmMimeType(
|
|
286
311
|
mimeType: string,
|
|
287
312
|
): { sampleRate: number; channels: number; bitsPerSample: number } | undefined {
|