@tanstack/ai-gemini 0.3.2 → 0.4.1

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/README.md CHANGED
@@ -45,6 +45,7 @@ A powerful, type-safe AI SDK for building AI-powered applications.
45
45
  - Headless chat state management with adapters (SSE, HTTP stream, custom)
46
46
  - Isomorphic type-safe tools with server/client execution
47
47
  - **Enhanced integration with TanStack Start** - Share implementations between AI tools and server functions
48
+ - **Observability events** - Structured, typed events for text, tools, image, speech, transcription, and video ([docs](./docs/guides/observability.md))
48
49
 
49
50
  ### <a href="https://tanstack.com/ai">Read the docs →</b></a>
50
51
 
@@ -1,6 +1,11 @@
1
- import { GoogleGenAI } from '@google/genai';
1
+ import { GeminiClientConfig } from '../utils.js';
2
2
  import { SummarizeAdapter } from '@tanstack/ai/adapters';
3
3
  import { StreamChunk, SummarizationOptions, SummarizationResult } from '@tanstack/ai';
4
+ /**
5
+ * Configuration for Gemini summarize adapter
6
+ */
7
+ export interface GeminiSummarizeConfig extends GeminiClientConfig {
8
+ }
4
9
  /**
5
10
  * Available Gemini models for summarization
6
11
  */
@@ -38,7 +43,7 @@ export declare class GeminiSummarizeAdapter<TModel extends GeminiSummarizeModel>
38
43
  providerOptions: GeminiSummarizeProviderOptions;
39
44
  };
40
45
  private client;
41
- constructor(apiKeyOrClient: string | GoogleGenAI, model: TModel, _options?: GeminiSummarizeAdapterOptions);
46
+ constructor(config: GeminiSummarizeConfig, model: TModel);
42
47
  summarize(options: SummarizationOptions): Promise<SummarizationResult>;
43
48
  summarizeStream(options: SummarizationOptions): AsyncIterable<StreamChunk>;
44
49
  private getFormatInstructions;
@@ -46,8 +51,8 @@ export declare class GeminiSummarizeAdapter<TModel extends GeminiSummarizeModel>
46
51
  /**
47
52
  * Creates a Gemini summarize adapter with explicit API key and model
48
53
  */
49
- export declare function createGeminiSummarize<TModel extends GeminiSummarizeModel>(apiKey: string, model: TModel, options?: GeminiSummarizeAdapterOptions): GeminiSummarizeAdapter<TModel>;
54
+ export declare function createGeminiSummarize<TModel extends GeminiSummarizeModel>(apiKey: string, model: TModel, config?: Omit<GeminiSummarizeConfig, 'apiKey'>): GeminiSummarizeAdapter<TModel>;
50
55
  /**
51
56
  * Creates a Gemini summarize adapter with API key from environment and required model
52
57
  */
53
- export declare function geminiSummarize<TModel extends GeminiSummarizeModel>(model: TModel, options?: GeminiSummarizeAdapterOptions): GeminiSummarizeAdapter<TModel>;
58
+ export declare function geminiSummarize<TModel extends GeminiSummarizeModel>(model: TModel, config?: Omit<GeminiSummarizeConfig, 'apiKey'>): GeminiSummarizeAdapter<TModel>;
@@ -7,10 +7,10 @@ const GeminiSummarizeModels = [
7
7
  "gemini-2.0-flash-lite"
8
8
  ];
9
9
  class GeminiSummarizeAdapter {
10
- constructor(apiKeyOrClient, model, _options = {}) {
10
+ constructor(config, model) {
11
11
  this.kind = "summarize";
12
12
  this.name = "gemini";
13
- this.client = typeof apiKeyOrClient === "string" ? createGeminiClient({ apiKey: apiKeyOrClient }) : apiKeyOrClient;
13
+ this.client = createGeminiClient(config);
14
14
  this.model = model;
15
15
  }
16
16
  async summarize(options) {
@@ -83,13 +83,12 @@ ${options.text}` }
83
83
  if (part.text) {
84
84
  accumulatedContent += part.text;
85
85
  yield {
86
- type: "content",
87
- id,
86
+ type: "TEXT_MESSAGE_CONTENT",
87
+ messageId: id,
88
88
  model,
89
89
  timestamp: Date.now(),
90
90
  delta: part.text,
91
- content: accumulatedContent,
92
- role: "assistant"
91
+ content: accumulatedContent
93
92
  };
94
93
  }
95
94
  }
@@ -97,8 +96,8 @@ ${options.text}` }
97
96
  const finishReason = chunk.candidates?.[0]?.finishReason;
98
97
  if (finishReason === FinishReason.STOP || finishReason === FinishReason.MAX_TOKENS || finishReason === FinishReason.SAFETY) {
99
98
  yield {
100
- type: "done",
101
- id,
99
+ type: "RUN_FINISHED",
100
+ runId: id,
102
101
  model,
103
102
  timestamp: Date.now(),
104
103
  finishReason: finishReason === FinishReason.STOP ? "stop" : finishReason === FinishReason.MAX_TOKENS ? "length" : "content_filter",
@@ -123,12 +122,12 @@ ${options.text}` }
123
122
  }
124
123
  }
125
124
  }
126
- function createGeminiSummarize(apiKey, model, options) {
127
- return new GeminiSummarizeAdapter(apiKey, model, options);
125
+ function createGeminiSummarize(apiKey, model, config) {
126
+ return new GeminiSummarizeAdapter({ ...config, apiKey }, model);
128
127
  }
129
- function geminiSummarize(model, options) {
128
+ function geminiSummarize(model, config) {
130
129
  const apiKey = getGeminiApiKeyFromEnv();
131
- return new GeminiSummarizeAdapter(apiKey, model, options);
130
+ return new GeminiSummarizeAdapter({ ...config, apiKey }, model);
132
131
  }
133
132
  export {
134
133
  GeminiSummarizeAdapter,
@@ -1 +1 @@
1
- {"version":3,"file":"summarize.js","sources":["../../../src/adapters/summarize.ts"],"sourcesContent":["import { FinishReason } from '@google/genai'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\n\nimport type { GoogleGenAI } from '@google/genai'\nimport type { SummarizeAdapter } from '@tanstack/ai/adapters'\nimport type {\n StreamChunk,\n SummarizationOptions,\n SummarizationResult,\n} from '@tanstack/ai'\n\n/**\n * Available Gemini models for summarization\n */\nexport const GeminiSummarizeModels = [\n 'gemini-2.0-flash',\n 'gemini-1.5-flash',\n 'gemini-1.5-pro',\n 'gemini-2.0-flash-lite',\n] as const\n\nexport type GeminiSummarizeModel = (typeof GeminiSummarizeModels)[number]\n\n/**\n * Provider-specific options for Gemini summarization\n */\nexport interface GeminiSummarizeProviderOptions {\n /** Generation configuration */\n generationConfig?: {\n temperature?: number\n topP?: number\n topK?: number\n maxOutputTokens?: number\n stopSequences?: Array<string>\n }\n /** Safety settings */\n safetySettings?: Array<{\n category: string\n threshold: string\n }>\n}\n\nexport interface GeminiSummarizeAdapterOptions {\n // Additional adapter options can be added here\n}\n\n/**\n * Gemini Summarize Adapter\n * A tree-shakeable summarization adapter for Google Gemini\n */\nexport class GeminiSummarizeAdapter<\n TModel extends GeminiSummarizeModel,\n> implements SummarizeAdapter<TModel, GeminiSummarizeProviderOptions> {\n readonly kind = 'summarize' as const\n readonly name = 'gemini' as const\n readonly model: TModel\n\n // Type-only property - never assigned at runtime\n declare '~types': {\n providerOptions: GeminiSummarizeProviderOptions\n }\n\n private client: GoogleGenAI\n\n constructor(\n apiKeyOrClient: string | GoogleGenAI,\n model: TModel,\n _options: GeminiSummarizeAdapterOptions = {},\n ) {\n this.client =\n typeof apiKeyOrClient === 'string'\n ? createGeminiClient({ apiKey: apiKeyOrClient })\n : apiKeyOrClient\n this.model = model\n }\n\n async summarize(options: SummarizationOptions): Promise<SummarizationResult> {\n const model = options.model\n\n // Build the system prompt based on format\n const formatInstructions = this.getFormatInstructions(options.style)\n const lengthInstructions = options.maxLength\n ? ` Keep the summary under ${options.maxLength} tokens.`\n : ''\n\n const systemPrompt = `You are a helpful assistant that summarizes text. ${formatInstructions}${lengthInstructions}`\n\n const response = await this.client.models.generateContent({\n model,\n contents: [\n {\n role: 'user',\n parts: [\n { text: `Please summarize the following:\\n\\n${options.text}` },\n ],\n },\n ],\n config: {\n systemInstruction: systemPrompt,\n },\n })\n\n const summary = response.text ?? ''\n const inputTokens = response.usageMetadata?.promptTokenCount ?? 0\n const outputTokens = response.usageMetadata?.candidatesTokenCount ?? 0\n\n return {\n id: generateId('sum'),\n model,\n summary,\n usage: {\n promptTokens: inputTokens,\n completionTokens: outputTokens,\n totalTokens: inputTokens + outputTokens,\n },\n }\n }\n\n async *summarizeStream(\n options: SummarizationOptions,\n ): AsyncIterable<StreamChunk> {\n const model = options.model\n const id = generateId('sum')\n let accumulatedContent = ''\n let inputTokens = 0\n let outputTokens = 0\n\n // Build the system prompt based on format\n const formatInstructions = this.getFormatInstructions(options.style)\n const lengthInstructions = options.maxLength\n ? ` Keep the summary under ${options.maxLength} words.`\n : ''\n\n const systemPrompt = `You are a helpful assistant that summarizes text. ${formatInstructions}${lengthInstructions}`\n\n const result = await this.client.models.generateContentStream({\n model,\n contents: [\n {\n role: 'user',\n parts: [\n { text: `Please summarize the following:\\n\\n${options.text}` },\n ],\n },\n ],\n config: {\n systemInstruction: systemPrompt,\n },\n })\n\n for await (const chunk of result) {\n // Track usage metadata\n if (chunk.usageMetadata) {\n inputTokens = chunk.usageMetadata.promptTokenCount ?? inputTokens\n outputTokens = chunk.usageMetadata.candidatesTokenCount ?? outputTokens\n }\n\n if (chunk.candidates?.[0]?.content?.parts) {\n for (const part of chunk.candidates[0].content.parts) {\n if (part.text) {\n accumulatedContent += part.text\n yield {\n type: 'content',\n id,\n model,\n timestamp: Date.now(),\n delta: part.text,\n content: accumulatedContent,\n role: 'assistant',\n }\n }\n }\n }\n\n // Check for finish reason\n const finishReason = chunk.candidates?.[0]?.finishReason\n if (\n finishReason === FinishReason.STOP ||\n finishReason === FinishReason.MAX_TOKENS ||\n finishReason === FinishReason.SAFETY\n ) {\n yield {\n type: 'done',\n id,\n model,\n timestamp: Date.now(),\n finishReason:\n finishReason === FinishReason.STOP\n ? 'stop'\n : finishReason === FinishReason.MAX_TOKENS\n ? 'length'\n : 'content_filter',\n usage: {\n promptTokens: inputTokens,\n completionTokens: outputTokens,\n totalTokens: inputTokens + outputTokens,\n },\n }\n }\n }\n }\n\n private getFormatInstructions(\n style?: 'paragraph' | 'bullet-points' | 'concise',\n ): string {\n switch (style) {\n case 'bullet-points':\n return 'Provide the summary as bullet points.'\n case 'concise':\n return 'Provide a very brief one or two sentence summary.'\n case 'paragraph':\n default:\n return 'Provide the summary in paragraph form.'\n }\n }\n}\n\n/**\n * Creates a Gemini summarize adapter with explicit API key and model\n */\nexport function createGeminiSummarize<TModel extends GeminiSummarizeModel>(\n apiKey: string,\n model: TModel,\n options?: GeminiSummarizeAdapterOptions,\n): GeminiSummarizeAdapter<TModel> {\n return new GeminiSummarizeAdapter(apiKey, model, options)\n}\n\n/**\n * Creates a Gemini summarize adapter with API key from environment and required model\n */\nexport function geminiSummarize<TModel extends GeminiSummarizeModel>(\n model: TModel,\n options?: GeminiSummarizeAdapterOptions,\n): GeminiSummarizeAdapter<TModel> {\n const apiKey = getGeminiApiKeyFromEnv()\n return new GeminiSummarizeAdapter(apiKey, model, options)\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"summarize.js","sources":["../../../src/adapters/summarize.ts"],"sourcesContent":["import { FinishReason } from '@google/genai'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport type { GoogleGenAI } from '@google/genai'\nimport type { GeminiClientConfig } from '../utils'\nimport type { SummarizeAdapter } from '@tanstack/ai/adapters'\nimport type {\n StreamChunk,\n SummarizationOptions,\n SummarizationResult,\n} from '@tanstack/ai'\n\n/**\n * Configuration for Gemini summarize adapter\n */\nexport interface GeminiSummarizeConfig extends GeminiClientConfig {}\n/**\n * Available Gemini models for summarization\n */\nexport const GeminiSummarizeModels = [\n 'gemini-2.0-flash',\n 'gemini-1.5-flash',\n 'gemini-1.5-pro',\n 'gemini-2.0-flash-lite',\n] as const\n\nexport type GeminiSummarizeModel = (typeof GeminiSummarizeModels)[number]\n\n/**\n * Provider-specific options for Gemini summarization\n */\nexport interface GeminiSummarizeProviderOptions {\n /** Generation configuration */\n generationConfig?: {\n temperature?: number\n topP?: number\n topK?: number\n maxOutputTokens?: number\n stopSequences?: Array<string>\n }\n /** Safety settings */\n safetySettings?: Array<{\n category: string\n threshold: string\n }>\n}\n\nexport interface GeminiSummarizeAdapterOptions {\n // Additional adapter options can be added here\n}\n\n/**\n * Gemini Summarize Adapter\n * A tree-shakeable summarization adapter for Google Gemini\n */\nexport class GeminiSummarizeAdapter<\n TModel extends GeminiSummarizeModel,\n> implements SummarizeAdapter<TModel, GeminiSummarizeProviderOptions> {\n readonly kind = 'summarize' as const\n readonly name = 'gemini' as const\n readonly model: TModel\n\n // Type-only property - never assigned at runtime\n declare '~types': {\n providerOptions: GeminiSummarizeProviderOptions\n }\n\n private client: GoogleGenAI\n\n constructor(config: GeminiSummarizeConfig, model: TModel) {\n this.client = createGeminiClient(config)\n this.model = model\n }\n\n async summarize(options: SummarizationOptions): Promise<SummarizationResult> {\n const model = options.model\n\n // Build the system prompt based on format\n const formatInstructions = this.getFormatInstructions(options.style)\n const lengthInstructions = options.maxLength\n ? ` Keep the summary under ${options.maxLength} tokens.`\n : ''\n\n const systemPrompt = `You are a helpful assistant that summarizes text. ${formatInstructions}${lengthInstructions}`\n\n const response = await this.client.models.generateContent({\n model,\n contents: [\n {\n role: 'user',\n parts: [\n { text: `Please summarize the following:\\n\\n${options.text}` },\n ],\n },\n ],\n config: {\n systemInstruction: systemPrompt,\n },\n })\n\n const summary = response.text ?? ''\n const inputTokens = response.usageMetadata?.promptTokenCount ?? 0\n const outputTokens = response.usageMetadata?.candidatesTokenCount ?? 0\n\n return {\n id: generateId('sum'),\n model,\n summary,\n usage: {\n promptTokens: inputTokens,\n completionTokens: outputTokens,\n totalTokens: inputTokens + outputTokens,\n },\n }\n }\n\n async *summarizeStream(\n options: SummarizationOptions,\n ): AsyncIterable<StreamChunk> {\n const model = options.model\n const id = generateId('sum')\n let accumulatedContent = ''\n let inputTokens = 0\n let outputTokens = 0\n\n // Build the system prompt based on format\n const formatInstructions = this.getFormatInstructions(options.style)\n const lengthInstructions = options.maxLength\n ? ` Keep the summary under ${options.maxLength} words.`\n : ''\n\n const systemPrompt = `You are a helpful assistant that summarizes text. ${formatInstructions}${lengthInstructions}`\n\n const result = await this.client.models.generateContentStream({\n model,\n contents: [\n {\n role: 'user',\n parts: [\n { text: `Please summarize the following:\\n\\n${options.text}` },\n ],\n },\n ],\n config: {\n systemInstruction: systemPrompt,\n },\n })\n\n for await (const chunk of result) {\n // Track usage metadata\n if (chunk.usageMetadata) {\n inputTokens = chunk.usageMetadata.promptTokenCount ?? inputTokens\n outputTokens = chunk.usageMetadata.candidatesTokenCount ?? outputTokens\n }\n\n if (chunk.candidates?.[0]?.content?.parts) {\n for (const part of chunk.candidates[0].content.parts) {\n if (part.text) {\n accumulatedContent += part.text\n yield {\n type: 'TEXT_MESSAGE_CONTENT',\n messageId: id,\n model,\n timestamp: Date.now(),\n delta: part.text,\n content: accumulatedContent,\n }\n }\n }\n }\n\n // Check for finish reason\n const finishReason = chunk.candidates?.[0]?.finishReason\n if (\n finishReason === FinishReason.STOP ||\n finishReason === FinishReason.MAX_TOKENS ||\n finishReason === FinishReason.SAFETY\n ) {\n yield {\n type: 'RUN_FINISHED',\n runId: id,\n model,\n timestamp: Date.now(),\n finishReason:\n finishReason === FinishReason.STOP\n ? 'stop'\n : finishReason === FinishReason.MAX_TOKENS\n ? 'length'\n : 'content_filter',\n usage: {\n promptTokens: inputTokens,\n completionTokens: outputTokens,\n totalTokens: inputTokens + outputTokens,\n },\n }\n }\n }\n }\n\n private getFormatInstructions(\n style?: 'paragraph' | 'bullet-points' | 'concise',\n ): string {\n switch (style) {\n case 'bullet-points':\n return 'Provide the summary as bullet points.'\n case 'concise':\n return 'Provide a very brief one or two sentence summary.'\n case 'paragraph':\n default:\n return 'Provide the summary in paragraph form.'\n }\n }\n}\n\n/**\n * Creates a Gemini summarize adapter with explicit API key and model\n */\nexport function createGeminiSummarize<TModel extends GeminiSummarizeModel>(\n apiKey: string,\n model: TModel,\n config?: Omit<GeminiSummarizeConfig, 'apiKey'>,\n): GeminiSummarizeAdapter<TModel> {\n return new GeminiSummarizeAdapter({ ...config, apiKey }, model)\n}\n\n/**\n * Creates a Gemini summarize adapter with API key from environment and required model\n */\nexport function geminiSummarize<TModel extends GeminiSummarizeModel>(\n model: TModel,\n config?: Omit<GeminiSummarizeConfig, 'apiKey'>,\n): GeminiSummarizeAdapter<TModel> {\n const apiKey = getGeminiApiKeyFromEnv()\n return new GeminiSummarizeAdapter({ ...config, apiKey }, model)\n}\n"],"names":[],"mappings":"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"}
@@ -17,8 +17,7 @@ class GeminiTextAdapter extends BaseTextAdapter {
17
17
  } catch (error) {
18
18
  const timestamp = Date.now();
19
19
  yield {
20
- type: "error",
21
- id: generateId(this.name),
20
+ type: "RUN_ERROR",
22
21
  model: options.model,
23
22
  timestamp,
24
23
  error: {
@@ -82,30 +81,65 @@ class GeminiTextAdapter extends BaseTextAdapter {
82
81
  let accumulatedContent = "";
83
82
  const toolCallMap = /* @__PURE__ */ new Map();
84
83
  let nextToolIndex = 0;
84
+ const runId = generateId(this.name);
85
+ const messageId = generateId(this.name);
86
+ let stepId = null;
87
+ let hasEmittedRunStarted = false;
88
+ let hasEmittedTextMessageStart = false;
89
+ let hasEmittedStepStarted = false;
85
90
  for await (const chunk of result) {
91
+ if (!hasEmittedRunStarted) {
92
+ hasEmittedRunStarted = true;
93
+ yield {
94
+ type: "RUN_STARTED",
95
+ runId,
96
+ model,
97
+ timestamp
98
+ };
99
+ }
86
100
  if (chunk.candidates?.[0]?.content?.parts) {
87
101
  const parts = chunk.candidates[0].content.parts;
88
102
  for (const part of parts) {
89
103
  if (part.text) {
90
104
  if (part.thought) {
105
+ if (!hasEmittedStepStarted) {
106
+ hasEmittedStepStarted = true;
107
+ stepId = generateId(this.name);
108
+ yield {
109
+ type: "STEP_STARTED",
110
+ stepId,
111
+ model,
112
+ timestamp,
113
+ stepType: "thinking"
114
+ };
115
+ }
91
116
  yield {
92
- type: "thinking",
93
- content: part.text,
94
- delta: part.text,
95
- id: generateId(this.name),
117
+ type: "STEP_FINISHED",
118
+ stepId: stepId || generateId(this.name),
96
119
  model,
97
- timestamp
120
+ timestamp,
121
+ delta: part.text,
122
+ content: part.text
98
123
  };
99
124
  } else {
125
+ if (!hasEmittedTextMessageStart) {
126
+ hasEmittedTextMessageStart = true;
127
+ yield {
128
+ type: "TEXT_MESSAGE_START",
129
+ messageId,
130
+ model,
131
+ timestamp,
132
+ role: "assistant"
133
+ };
134
+ }
100
135
  accumulatedContent += part.text;
101
136
  yield {
102
- type: "content",
103
- id: generateId(this.name),
137
+ type: "TEXT_MESSAGE_CONTENT",
138
+ messageId,
104
139
  model,
105
140
  timestamp,
106
141
  delta: part.text,
107
- content: accumulatedContent,
108
- role: "assistant"
142
+ content: accumulatedContent
109
143
  };
110
144
  }
111
145
  }
@@ -118,7 +152,8 @@ class GeminiTextAdapter extends BaseTextAdapter {
118
152
  toolCallData = {
119
153
  name: functionCall.name || "",
120
154
  args: typeof functionArgs === "string" ? functionArgs : JSON.stringify(functionArgs),
121
- index: nextToolIndex++
155
+ index: nextToolIndex++,
156
+ started: false
122
157
  };
123
158
  toolCallMap.set(toolCallId, toolCallData);
124
159
  } else {
@@ -131,33 +166,46 @@ class GeminiTextAdapter extends BaseTextAdapter {
131
166
  toolCallData.args = typeof functionArgs === "string" ? functionArgs : JSON.stringify(functionArgs);
132
167
  }
133
168
  }
169
+ if (!toolCallData.started) {
170
+ toolCallData.started = true;
171
+ yield {
172
+ type: "TOOL_CALL_START",
173
+ toolCallId,
174
+ toolName: toolCallData.name,
175
+ model,
176
+ timestamp,
177
+ index: toolCallData.index
178
+ };
179
+ }
134
180
  yield {
135
- type: "tool_call",
136
- id: generateId(this.name),
181
+ type: "TOOL_CALL_ARGS",
182
+ toolCallId,
137
183
  model,
138
184
  timestamp,
139
- toolCall: {
140
- id: toolCallId,
141
- type: "function",
142
- function: {
143
- name: toolCallData.name,
144
- arguments: toolCallData.args
145
- }
146
- },
147
- index: toolCallData.index
185
+ delta: toolCallData.args,
186
+ args: toolCallData.args
148
187
  };
149
188
  }
150
189
  }
151
190
  } else if (chunk.data) {
191
+ if (!hasEmittedTextMessageStart) {
192
+ hasEmittedTextMessageStart = true;
193
+ yield {
194
+ type: "TEXT_MESSAGE_START",
195
+ messageId,
196
+ model,
197
+ timestamp,
198
+ role: "assistant"
199
+ };
200
+ }
152
201
  accumulatedContent += chunk.data;
153
202
  yield {
154
- type: "content",
155
- id: generateId(this.name),
203
+ type: "TEXT_MESSAGE_CONTENT",
204
+ messageId,
156
205
  model,
157
206
  timestamp,
158
207
  delta: chunk.data,
159
- content: accumulatedContent,
160
- role: "assistant"
208
+ content: accumulatedContent
161
209
  };
162
210
  }
163
211
  if (chunk.candidates?.[0]?.finishReason) {
@@ -169,44 +217,78 @@ class GeminiTextAdapter extends BaseTextAdapter {
169
217
  if (functionCall) {
170
218
  const toolCallId = functionCall.id || `${functionCall.name}_${Date.now()}_${nextToolIndex}`;
171
219
  const functionArgs = functionCall.args || {};
220
+ const argsString = typeof functionArgs === "string" ? functionArgs : JSON.stringify(functionArgs);
172
221
  toolCallMap.set(toolCallId, {
173
222
  name: functionCall.name || "",
174
- args: typeof functionArgs === "string" ? functionArgs : JSON.stringify(functionArgs),
175
- index: nextToolIndex++
223
+ args: argsString,
224
+ index: nextToolIndex++,
225
+ started: true
176
226
  });
177
227
  yield {
178
- type: "tool_call",
179
- id: generateId(this.name),
228
+ type: "TOOL_CALL_START",
229
+ toolCallId,
230
+ toolName: functionCall.name || "",
180
231
  model,
181
232
  timestamp,
182
- toolCall: {
183
- id: toolCallId,
184
- type: "function",
185
- function: {
186
- name: functionCall.name || "",
187
- arguments: typeof functionArgs === "string" ? functionArgs : JSON.stringify(functionArgs)
188
- }
189
- },
190
233
  index: nextToolIndex - 1
191
234
  };
235
+ let parsedInput = {};
236
+ try {
237
+ parsedInput = typeof functionArgs === "string" ? JSON.parse(functionArgs) : functionArgs;
238
+ } catch {
239
+ parsedInput = {};
240
+ }
241
+ yield {
242
+ type: "TOOL_CALL_END",
243
+ toolCallId,
244
+ toolName: functionCall.name || "",
245
+ model,
246
+ timestamp,
247
+ input: parsedInput
248
+ };
192
249
  }
193
250
  }
194
251
  }
195
252
  }
253
+ for (const [toolCallId, toolCallData] of toolCallMap.entries()) {
254
+ let parsedInput = {};
255
+ try {
256
+ parsedInput = JSON.parse(toolCallData.args);
257
+ } catch {
258
+ parsedInput = {};
259
+ }
260
+ yield {
261
+ type: "TOOL_CALL_END",
262
+ toolCallId,
263
+ toolName: toolCallData.name,
264
+ model,
265
+ timestamp,
266
+ input: parsedInput
267
+ };
268
+ }
196
269
  if (finishReason === FinishReason.MAX_TOKENS) {
197
270
  yield {
198
- type: "error",
199
- id: generateId(this.name),
271
+ type: "RUN_ERROR",
272
+ runId,
200
273
  model,
201
274
  timestamp,
202
275
  error: {
203
- message: "The response was cut off because the maximum token limit was reached."
276
+ message: "The response was cut off because the maximum token limit was reached.",
277
+ code: "max_tokens"
204
278
  }
205
279
  };
206
280
  }
281
+ if (hasEmittedTextMessageStart) {
282
+ yield {
283
+ type: "TEXT_MESSAGE_END",
284
+ messageId,
285
+ model,
286
+ timestamp
287
+ };
288
+ }
207
289
  yield {
208
- type: "done",
209
- id: generateId(this.name),
290
+ type: "RUN_FINISHED",
291
+ runId,
210
292
  model,
211
293
  timestamp,
212
294
  finishReason: toolCallMap.size > 0 ? "tool_calls" : "stop",
@@ -220,18 +302,6 @@ class GeminiTextAdapter extends BaseTextAdapter {
220
302
  }
221
303
  }
222
304
  convertContentPartToGemini(part) {
223
- const getDefaultFileType = (part2) => {
224
- switch (part2) {
225
- case "image":
226
- return "image/jpeg";
227
- case "audio":
228
- return "audio/mp3";
229
- case "video":
230
- return "video/mp4";
231
- case "document":
232
- return "application/pdf";
233
- }
234
- };
235
305
  switch (part.type) {
236
306
  case "text":
237
307
  return { text: part.content };
@@ -239,19 +309,24 @@ class GeminiTextAdapter extends BaseTextAdapter {
239
309
  case "audio":
240
310
  case "video":
241
311
  case "document": {
242
- const metadata = part.metadata;
243
312
  if (part.source.type === "data") {
244
313
  return {
245
314
  inlineData: {
246
315
  data: part.source.value,
247
- mimeType: metadata?.mimeType ?? getDefaultFileType(part.type)
316
+ mimeType: part.source.mimeType
248
317
  }
249
318
  };
250
319
  } else {
320
+ const defaultMimeType = {
321
+ image: "image/jpeg",
322
+ audio: "audio/mp3",
323
+ video: "video/mp4",
324
+ document: "application/pdf"
325
+ }[part.type];
251
326
  return {
252
327
  fileData: {
253
328
  fileUri: part.source.value,
254
- mimeType: metadata?.mimeType ?? getDefaultFileType(part.type)
329
+ mimeType: part.source.mimeType ?? defaultMimeType
255
330
  }
256
331
  };
257
332
  }
@@ -1 +1 @@
1
- {"version":3,"file":"text.js","sources":["../../../src/adapters/text.ts"],"sourcesContent":["import { FinishReason } from '@google/genai'\nimport { BaseTextAdapter } from '@tanstack/ai/adapters'\nimport { convertToolsToProviderFormat } from '../tools/tool-converter'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport type {\n GEMINI_MODELS,\n GeminiChatModelProviderOptionsByName,\n GeminiModelInputModalitiesByName,\n} from '../model-meta'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type {\n GenerateContentParameters,\n GenerateContentResponse,\n GoogleGenAI,\n Part,\n ThinkingLevel,\n} from '@google/genai'\nimport type {\n ContentPart,\n Modality,\n ModelMessage,\n StreamChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type { ExternalTextProviderOptions } from '../text/text-provider-options'\nimport type {\n GeminiAudioMetadata,\n GeminiDocumentMetadata,\n GeminiImageMetadata,\n GeminiMessageMetadataByModality,\n GeminiVideoMetadata,\n} from '../message-types'\nimport type { GeminiClientConfig } from '../utils'\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// 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 object = ResolveProviderOptions<TModel>,\n TInputModalities extends ReadonlyArray<Modality> =\n ResolveInputModalities<TModel>,\n> extends BaseTextAdapter<\n TModel,\n TProviderOptions,\n TInputModalities,\n GeminiMessageMetadataByModality\n> {\n readonly kind = 'text' as const\n readonly name = 'gemini' as const\n\n private client: GoogleGenAI\n\n constructor(config: GeminiTextConfig, model: TModel) {\n super({}, model)\n this.client = createGeminiClient(config)\n }\n\n async *chatStream(\n options: TextOptions<GeminiTextProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n const mappedOptions = this.mapCommonOptionsToGemini(options)\n\n try {\n const result =\n await this.client.models.generateContentStream(mappedOptions)\n\n yield* this.processStreamChunks(result, options.model)\n } catch (error) {\n const timestamp = Date.now()\n yield {\n type: 'error',\n id: generateId(this.name),\n model: options.model,\n timestamp,\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\n const mappedOptions = this.mapCommonOptionsToGemini(chatOptions)\n\n try {\n // Add structured output configuration\n const result = await this.client.models.generateContent({\n ...mappedOptions,\n config: {\n ...mappedOptions.config,\n responseMimeType: 'application/json',\n responseSchema: outputSchema,\n },\n })\n\n // Extract text content from the response\n const rawText = this.extractTextFromResponse(result)\n\n // Parse the JSON response\n let parsed: unknown\n try {\n parsed = JSON.parse(rawText)\n } catch {\n throw new Error(\n `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`,\n )\n }\n\n return {\n data: parsed,\n rawText,\n }\n } catch (error) {\n throw new Error(\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during structured output generation.',\n )\n }\n }\n\n /**\n * Extract text content from a non-streaming response\n */\n private extractTextFromResponse(response: GenerateContentResponse): string {\n let textContent = ''\n\n if (response.candidates?.[0]?.content?.parts) {\n for (const part of response.candidates[0].content.parts) {\n if (part.text) {\n textContent += part.text\n }\n }\n }\n\n return textContent\n }\n\n private async *processStreamChunks(\n result: AsyncGenerator<GenerateContentResponse, unknown, unknown>,\n model: string,\n ): AsyncIterable<StreamChunk> {\n const timestamp = Date.now()\n let accumulatedContent = ''\n const toolCallMap = new Map<\n string,\n { name: string; args: string; index: number }\n >()\n let nextToolIndex = 0\n\n for await (const chunk of result) {\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 yield {\n type: 'thinking',\n content: part.text,\n delta: part.text,\n id: generateId(this.name),\n model,\n timestamp,\n }\n } else {\n accumulatedContent += part.text\n yield {\n type: 'content',\n id: generateId(this.name),\n model,\n timestamp,\n delta: part.text,\n content: accumulatedContent,\n role: 'assistant',\n }\n }\n }\n\n const functionCall = part.functionCall\n if (functionCall) {\n const toolCallId =\n functionCall.id ||\n `${functionCall.name}_${Date.now()}_${nextToolIndex}`\n const functionArgs = functionCall.args || {}\n\n let toolCallData = toolCallMap.get(toolCallId)\n if (!toolCallData) {\n toolCallData = {\n name: functionCall.name || '',\n args:\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs),\n index: nextToolIndex++,\n }\n toolCallMap.set(toolCallId, toolCallData)\n } else {\n try {\n const existingArgs = JSON.parse(toolCallData.args)\n const newArgs =\n typeof functionArgs === 'string'\n ? JSON.parse(functionArgs)\n : functionArgs\n const mergedArgs = { ...existingArgs, ...newArgs }\n toolCallData.args = JSON.stringify(mergedArgs)\n } catch {\n toolCallData.args =\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs)\n }\n }\n\n yield {\n type: 'tool_call',\n id: generateId(this.name),\n model,\n timestamp,\n toolCall: {\n id: toolCallId,\n type: 'function',\n function: {\n name: toolCallData.name,\n arguments: toolCallData.args,\n },\n },\n index: toolCallData.index,\n }\n }\n }\n } else if (chunk.data) {\n accumulatedContent += chunk.data\n yield {\n type: 'content',\n id: generateId(this.name),\n model,\n timestamp,\n delta: chunk.data,\n content: accumulatedContent,\n role: 'assistant',\n }\n }\n\n if (chunk.candidates?.[0]?.finishReason) {\n const finishReason = chunk.candidates[0].finishReason\n\n if (finishReason === FinishReason.UNEXPECTED_TOOL_CALL) {\n if (chunk.candidates[0].content?.parts) {\n for (const part of chunk.candidates[0].content.parts) {\n const functionCall = part.functionCall\n if (functionCall) {\n const toolCallId =\n functionCall.id ||\n `${functionCall.name}_${Date.now()}_${nextToolIndex}`\n const functionArgs = functionCall.args || {}\n\n toolCallMap.set(toolCallId, {\n name: functionCall.name || '',\n args:\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs),\n index: nextToolIndex++,\n })\n\n yield {\n type: 'tool_call',\n id: generateId(this.name),\n model,\n timestamp,\n toolCall: {\n id: toolCallId,\n type: 'function',\n function: {\n name: functionCall.name || '',\n arguments:\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs),\n },\n },\n index: nextToolIndex - 1,\n }\n }\n }\n }\n }\n if (finishReason === FinishReason.MAX_TOKENS) {\n yield {\n type: 'error',\n id: generateId(this.name),\n model,\n timestamp,\n error: {\n message:\n 'The response was cut off because the maximum token limit was reached.',\n },\n }\n }\n\n yield {\n type: 'done',\n id: generateId(this.name),\n model,\n timestamp,\n finishReason: toolCallMap.size > 0 ? 'tool_calls' : 'stop',\n usage: chunk.usageMetadata\n ? {\n promptTokens: chunk.usageMetadata.promptTokenCount ?? 0,\n completionTokens: chunk.usageMetadata.candidatesTokenCount ?? 0,\n totalTokens: chunk.usageMetadata.totalTokenCount ?? 0,\n }\n : undefined,\n }\n }\n }\n }\n\n private convertContentPartToGemini(part: ContentPart): Part {\n const getDefaultFileType = (\n part: 'image' | 'audio' | 'video' | 'document',\n ) => {\n switch (part) {\n case 'image':\n return 'image/jpeg'\n case 'audio':\n return 'audio/mp3'\n case 'video':\n return 'video/mp4'\n case 'document':\n return 'application/pdf'\n }\n }\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 metadata = part.metadata as\n | GeminiDocumentMetadata\n | GeminiImageMetadata\n | GeminiVideoMetadata\n | GeminiAudioMetadata\n | undefined\n if (part.source.type === 'data') {\n return {\n inlineData: {\n data: part.source.value,\n mimeType: metadata?.mimeType ?? getDefaultFileType(part.type),\n },\n }\n } else {\n return {\n fileData: {\n fileUri: part.source.value,\n mimeType: metadata?.mimeType ?? getDefaultFileType(part.type),\n },\n }\n }\n }\n default: {\n const _exhaustiveCheck: never = part\n throw new Error(\n `Unsupported content part type: ${(_exhaustiveCheck as ContentPart).type}`,\n )\n }\n }\n }\n\n private formatMessages(\n messages: Array<ModelMessage>,\n ): GenerateContentParameters['contents'] {\n return 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) {\n parts.push({ text: msg.content })\n }\n\n if (msg.role === 'assistant' && msg.toolCalls?.length) {\n for (const toolCall of msg.toolCalls) {\n let parsedArgs: Record<string, unknown> = {}\n try {\n parsedArgs = toolCall.function.arguments\n ? (JSON.parse(toolCall.function.arguments) as Record<\n string,\n unknown\n >)\n : {}\n } catch {\n parsedArgs = toolCall.function.arguments as unknown as Record<\n string,\n unknown\n >\n }\n\n parts.push({\n functionCall: {\n name: toolCall.function.name,\n args: parsedArgs,\n },\n })\n }\n }\n\n if (msg.role === 'tool' && msg.toolCallId) {\n parts.push({\n functionResponse: {\n name: msg.toolCallId,\n response: {\n content: msg.content || '',\n },\n },\n })\n }\n\n return {\n role,\n parts: parts.length > 0 ? parts : [{ text: '' }],\n }\n })\n }\n\n private mapCommonOptionsToGemini(\n options: TextOptions<GeminiTextProviderOptions>,\n ) {\n const modelOpts = options.modelOptions\n const thinkingConfig = modelOpts?.thinkingConfig\n const requestOptions: GenerateContentParameters = {\n model: options.model,\n contents: this.formatMessages(options.messages),\n config: {\n ...modelOpts,\n temperature: options.temperature,\n topP: options.topP,\n maxOutputTokens: options.maxTokens,\n thinkingConfig: thinkingConfig\n ? {\n ...thinkingConfig,\n thinkingLevel: thinkingConfig.thinkingLevel\n ? // Enum is provided by the SDK, we use it for the type but cast it to string constants, here we just cast them back\n (thinkingConfig.thinkingLevel as ThinkingLevel)\n : undefined,\n }\n : undefined,\n systemInstruction: options.systemPrompts?.join('\\n'),\n tools: convertToolsToProviderFormat(options.tools),\n },\n }\n\n return requestOptions\n }\n}\n\n/**\n * Creates a Gemini text adapter with explicit API key.\n * Type resolution happens here at the call site.\n */\nexport function createGeminiChat<TModel extends (typeof GEMINI_MODELS)[number]>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiTextConfig, 'apiKey'>,\n): GeminiTextAdapter<\n TModel,\n ResolveProviderOptions<TModel>,\n ResolveInputModalities<TModel>\n> {\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> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiChat(model, apiKey, config)\n}\n"],"names":["part"],"mappings":"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"}
1
+ {"version":3,"file":"text.js","sources":["../../../src/adapters/text.ts"],"sourcesContent":["import { FinishReason } from '@google/genai'\nimport { BaseTextAdapter } from '@tanstack/ai/adapters'\nimport { convertToolsToProviderFormat } from '../tools/tool-converter'\nimport {\n createGeminiClient,\n generateId,\n getGeminiApiKeyFromEnv,\n} from '../utils'\nimport type {\n GEMINI_MODELS,\n GeminiChatModelProviderOptionsByName,\n GeminiModelInputModalitiesByName,\n} from '../model-meta'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type {\n GenerateContentParameters,\n GenerateContentResponse,\n GoogleGenAI,\n Part,\n ThinkingLevel,\n} from '@google/genai'\nimport type {\n ContentPart,\n Modality,\n ModelMessage,\n StreamChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type { ExternalTextProviderOptions } from '../text/text-provider-options'\nimport type { GeminiMessageMetadataByModality } from '../message-types'\nimport type { GeminiClientConfig } from '../utils'\n\n/**\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// 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 object = ResolveProviderOptions<TModel>,\n TInputModalities extends ReadonlyArray<Modality> =\n ResolveInputModalities<TModel>,\n> extends BaseTextAdapter<\n TModel,\n TProviderOptions,\n TInputModalities,\n GeminiMessageMetadataByModality\n> {\n readonly kind = 'text' as const\n readonly name = 'gemini' as const\n\n private client: GoogleGenAI\n\n constructor(config: GeminiTextConfig, model: TModel) {\n super({}, model)\n this.client = createGeminiClient(config)\n }\n\n async *chatStream(\n options: TextOptions<GeminiTextProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n const mappedOptions = this.mapCommonOptionsToGemini(options)\n\n try {\n const result =\n await this.client.models.generateContentStream(mappedOptions)\n\n yield* this.processStreamChunks(result, options.model)\n } catch (error) {\n const timestamp = Date.now()\n yield {\n type: 'RUN_ERROR',\n model: options.model,\n timestamp,\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\n const mappedOptions = this.mapCommonOptionsToGemini(chatOptions)\n\n try {\n // Add structured output configuration\n const result = await this.client.models.generateContent({\n ...mappedOptions,\n config: {\n ...mappedOptions.config,\n responseMimeType: 'application/json',\n responseSchema: outputSchema,\n },\n })\n\n // Extract text content from the response\n const rawText = this.extractTextFromResponse(result)\n\n // Parse the JSON response\n let parsed: unknown\n try {\n parsed = JSON.parse(rawText)\n } catch {\n throw new Error(\n `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`,\n )\n }\n\n return {\n data: parsed,\n rawText,\n }\n } catch (error) {\n throw new Error(\n error instanceof Error\n ? error.message\n : 'An unknown error occurred during structured output generation.',\n )\n }\n }\n\n /**\n * Extract text content from a non-streaming response\n */\n private extractTextFromResponse(response: GenerateContentResponse): string {\n let textContent = ''\n\n if (response.candidates?.[0]?.content?.parts) {\n for (const part of response.candidates[0].content.parts) {\n if (part.text) {\n textContent += part.text\n }\n }\n }\n\n return textContent\n }\n\n private async *processStreamChunks(\n result: AsyncGenerator<GenerateContentResponse, unknown, unknown>,\n model: string,\n ): AsyncIterable<StreamChunk> {\n const timestamp = Date.now()\n let accumulatedContent = ''\n const toolCallMap = new Map<\n string,\n { name: string; args: string; index: number; started: boolean }\n >()\n let nextToolIndex = 0\n\n // AG-UI lifecycle tracking\n const runId = generateId(this.name)\n const messageId = generateId(this.name)\n let stepId: string | null = null\n let hasEmittedRunStarted = false\n let hasEmittedTextMessageStart = false\n let hasEmittedStepStarted = false\n\n for await (const chunk of result) {\n // Emit RUN_STARTED on first chunk\n if (!hasEmittedRunStarted) {\n hasEmittedRunStarted = true\n yield {\n type: 'RUN_STARTED',\n runId,\n model,\n timestamp,\n }\n }\n\n if (chunk.candidates?.[0]?.content?.parts) {\n const parts = chunk.candidates[0].content.parts\n\n for (const part of parts) {\n if (part.text) {\n if (part.thought) {\n // Emit STEP_STARTED on first thinking content\n if (!hasEmittedStepStarted) {\n hasEmittedStepStarted = true\n stepId = generateId(this.name)\n yield {\n type: 'STEP_STARTED',\n stepId,\n model,\n timestamp,\n stepType: 'thinking',\n }\n }\n\n yield {\n type: 'STEP_FINISHED',\n stepId: stepId || generateId(this.name),\n model,\n timestamp,\n delta: part.text,\n content: part.text,\n }\n } else {\n // Emit TEXT_MESSAGE_START on first text content\n if (!hasEmittedTextMessageStart) {\n hasEmittedTextMessageStart = true\n yield {\n type: 'TEXT_MESSAGE_START',\n messageId,\n model,\n timestamp,\n role: 'assistant',\n }\n }\n\n accumulatedContent += part.text\n yield {\n type: 'TEXT_MESSAGE_CONTENT',\n messageId,\n model,\n timestamp,\n delta: part.text,\n content: accumulatedContent,\n }\n }\n }\n\n const functionCall = part.functionCall\n if (functionCall) {\n const toolCallId =\n functionCall.id ||\n `${functionCall.name}_${Date.now()}_${nextToolIndex}`\n const functionArgs = functionCall.args || {}\n\n let toolCallData = toolCallMap.get(toolCallId)\n if (!toolCallData) {\n toolCallData = {\n name: functionCall.name || '',\n args:\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs),\n index: nextToolIndex++,\n started: false,\n }\n toolCallMap.set(toolCallId, toolCallData)\n } else {\n try {\n const existingArgs = JSON.parse(toolCallData.args)\n const newArgs =\n typeof functionArgs === 'string'\n ? JSON.parse(functionArgs)\n : functionArgs\n const mergedArgs = { ...existingArgs, ...newArgs }\n toolCallData.args = JSON.stringify(mergedArgs)\n } catch {\n toolCallData.args =\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs)\n }\n }\n\n // Emit TOOL_CALL_START if not already started\n if (!toolCallData.started) {\n toolCallData.started = true\n yield {\n type: 'TOOL_CALL_START',\n toolCallId,\n toolName: toolCallData.name,\n model,\n timestamp,\n index: toolCallData.index,\n }\n }\n\n // Emit TOOL_CALL_ARGS\n yield {\n type: 'TOOL_CALL_ARGS',\n toolCallId,\n model,\n timestamp,\n delta: toolCallData.args,\n args: toolCallData.args,\n }\n }\n }\n } else if (chunk.data) {\n // Emit TEXT_MESSAGE_START on first text content\n if (!hasEmittedTextMessageStart) {\n hasEmittedTextMessageStart = true\n yield {\n type: 'TEXT_MESSAGE_START',\n messageId,\n model,\n timestamp,\n role: 'assistant',\n }\n }\n\n accumulatedContent += chunk.data\n yield {\n type: 'TEXT_MESSAGE_CONTENT',\n messageId,\n model,\n timestamp,\n delta: chunk.data,\n content: accumulatedContent,\n }\n }\n\n if (chunk.candidates?.[0]?.finishReason) {\n const finishReason = chunk.candidates[0].finishReason\n\n if (finishReason === FinishReason.UNEXPECTED_TOOL_CALL) {\n if (chunk.candidates[0].content?.parts) {\n for (const part of chunk.candidates[0].content.parts) {\n const functionCall = part.functionCall\n if (functionCall) {\n const toolCallId =\n functionCall.id ||\n `${functionCall.name}_${Date.now()}_${nextToolIndex}`\n const functionArgs = functionCall.args || {}\n\n const argsString =\n typeof functionArgs === 'string'\n ? functionArgs\n : JSON.stringify(functionArgs)\n\n toolCallMap.set(toolCallId, {\n name: functionCall.name || '',\n args: argsString,\n index: nextToolIndex++,\n started: true,\n })\n\n // Emit TOOL_CALL_START\n yield {\n type: 'TOOL_CALL_START',\n toolCallId,\n toolName: functionCall.name || '',\n model,\n timestamp,\n index: nextToolIndex - 1,\n }\n\n // Emit TOOL_CALL_END with parsed input\n let parsedInput: unknown = {}\n try {\n parsedInput =\n typeof functionArgs === 'string'\n ? JSON.parse(functionArgs)\n : functionArgs\n } catch {\n parsedInput = {}\n }\n\n yield {\n type: 'TOOL_CALL_END',\n toolCallId,\n toolName: functionCall.name || '',\n model,\n timestamp,\n input: parsedInput,\n }\n }\n }\n }\n }\n\n // Emit TOOL_CALL_END for all tracked tool calls\n for (const [toolCallId, toolCallData] of toolCallMap.entries()) {\n let parsedInput: unknown = {}\n try {\n parsedInput = JSON.parse(toolCallData.args)\n } catch {\n parsedInput = {}\n }\n\n yield {\n type: 'TOOL_CALL_END',\n toolCallId,\n toolName: toolCallData.name,\n model,\n timestamp,\n input: parsedInput,\n }\n }\n\n if (finishReason === FinishReason.MAX_TOKENS) {\n yield {\n type: 'RUN_ERROR',\n runId,\n model,\n timestamp,\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 // Emit TEXT_MESSAGE_END if we had text content\n if (hasEmittedTextMessageStart) {\n yield {\n type: 'TEXT_MESSAGE_END',\n messageId,\n model,\n timestamp,\n }\n }\n\n yield {\n type: 'RUN_FINISHED',\n runId,\n model,\n timestamp,\n finishReason: toolCallMap.size > 0 ? 'tool_calls' : 'stop',\n usage: chunk.usageMetadata\n ? {\n promptTokens: chunk.usageMetadata.promptTokenCount ?? 0,\n completionTokens: chunk.usageMetadata.candidatesTokenCount ?? 0,\n totalTokens: chunk.usageMetadata.totalTokenCount ?? 0,\n }\n : undefined,\n }\n }\n }\n }\n\n private convertContentPartToGemini(part: ContentPart): Part {\n switch (part.type) {\n case 'text':\n return { text: part.content }\n case 'image':\n case 'audio':\n case 'video':\n case 'document': {\n if (part.source.type === 'data') {\n return {\n inlineData: {\n data: part.source.value,\n mimeType: part.source.mimeType,\n },\n }\n } else {\n // For URL sources, use provided mimeType or fall back to reasonable defaults\n const defaultMimeType = {\n image: 'image/jpeg',\n audio: 'audio/mp3',\n video: 'video/mp4',\n document: 'application/pdf',\n }[part.type]\n\n return {\n fileData: {\n fileUri: part.source.value,\n mimeType: part.source.mimeType ?? defaultMimeType,\n },\n }\n }\n }\n default: {\n const _exhaustiveCheck: never = part\n throw new Error(\n `Unsupported content part type: ${(_exhaustiveCheck as ContentPart).type}`,\n )\n }\n }\n }\n\n private formatMessages(\n messages: Array<ModelMessage>,\n ): GenerateContentParameters['contents'] {\n return 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) {\n parts.push({ text: msg.content })\n }\n\n if (msg.role === 'assistant' && msg.toolCalls?.length) {\n for (const toolCall of msg.toolCalls) {\n let parsedArgs: Record<string, unknown> = {}\n try {\n parsedArgs = toolCall.function.arguments\n ? (JSON.parse(toolCall.function.arguments) as Record<\n string,\n unknown\n >)\n : {}\n } catch {\n parsedArgs = toolCall.function.arguments as unknown as Record<\n string,\n unknown\n >\n }\n\n parts.push({\n functionCall: {\n name: toolCall.function.name,\n args: parsedArgs,\n },\n })\n }\n }\n\n if (msg.role === 'tool' && msg.toolCallId) {\n parts.push({\n functionResponse: {\n name: msg.toolCallId,\n response: {\n content: msg.content || '',\n },\n },\n })\n }\n\n return {\n role,\n parts: parts.length > 0 ? parts : [{ text: '' }],\n }\n })\n }\n\n private mapCommonOptionsToGemini(\n options: TextOptions<GeminiTextProviderOptions>,\n ) {\n const modelOpts = options.modelOptions\n const thinkingConfig = modelOpts?.thinkingConfig\n const requestOptions: GenerateContentParameters = {\n model: options.model,\n contents: this.formatMessages(options.messages),\n config: {\n ...modelOpts,\n temperature: options.temperature,\n topP: options.topP,\n maxOutputTokens: options.maxTokens,\n thinkingConfig: thinkingConfig\n ? {\n ...thinkingConfig,\n thinkingLevel: thinkingConfig.thinkingLevel\n ? // Enum is provided by the SDK, we use it for the type but cast it to string constants, here we just cast them back\n (thinkingConfig.thinkingLevel as ThinkingLevel)\n : undefined,\n }\n : undefined,\n systemInstruction: options.systemPrompts?.join('\\n'),\n tools: convertToolsToProviderFormat(options.tools),\n },\n }\n\n return requestOptions\n }\n}\n\n/**\n * Creates a Gemini text adapter with explicit API key.\n * Type resolution happens here at the call site.\n */\nexport function createGeminiChat<TModel extends (typeof GEMINI_MODELS)[number]>(\n model: TModel,\n apiKey: string,\n config?: Omit<GeminiTextConfig, 'apiKey'>,\n): GeminiTextAdapter<\n TModel,\n ResolveProviderOptions<TModel>,\n ResolveInputModalities<TModel>\n> {\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> {\n const apiKey = getGeminiApiKeyFromEnv()\n return createGeminiChat(model, apiKey, config)\n}\n"],"names":[],"mappings":"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"}
@@ -16,4 +16,4 @@ export type { GeminiTTSVoice } from './model-meta.js';
16
16
  export type { GeminiChatModelProviderOptionsByName, GeminiModelInputModalitiesByName, } from './model-meta.js';
17
17
  export type { GeminiStructuredOutputOptions, GeminiThinkingOptions, } from './text/text-provider-options.js';
18
18
  export type { GoogleGeminiTool } from './tools/index.js';
19
- export type { GeminiTextMetadata, GeminiImageMetadata, GeminiAudioMetadata, GeminiVideoMetadata, GeminiDocumentMetadata, GeminiImageMimeType, GeminiAudioMimeType, GeminiVideoMimeType, GeminiDocumentMimeType, GeminiMessageMetadataByModality, } from './message-types.js';
19
+ export type { GeminiTextMetadata, GeminiImageMetadata, GeminiAudioMetadata, GeminiVideoMetadata, GeminiDocumentMetadata, GeminiMessageMetadataByModality, } from './message-types.js';
@@ -1,5 +1,5 @@
1
- import { GoogleGenAI } from '@google/genai';
2
- export interface GeminiClientConfig {
1
+ import { GoogleGenAI, GoogleGenAIOptions } from '@google/genai';
2
+ export interface GeminiClientConfig extends GoogleGenAIOptions {
3
3
  apiKey: string;
4
4
  }
5
5
  /**
@@ -1,6 +1,7 @@
1
1
  import { GoogleGenAI } from "@google/genai";
2
2
  function createGeminiClient(config) {
3
3
  return new GoogleGenAI({
4
+ ...config,
4
5
  apiKey: config.apiKey
5
6
  });
6
7
  }
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sources":["../../../src/utils/client.ts"],"sourcesContent":["import { GoogleGenAI } from '@google/genai'\n\nexport interface GeminiClientConfig {\n apiKey: string\n}\n\n/**\n * Creates a Google Generative AI client instance\n */\nexport function createGeminiClient(config: GeminiClientConfig): GoogleGenAI {\n return new GoogleGenAI({\n apiKey: config.apiKey,\n })\n}\n\n/**\n * Gets Google API key from environment variables\n * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found\n */\nexport function getGeminiApiKeyFromEnv(): string {\n const env =\n typeof globalThis !== 'undefined' && (globalThis as any).window?.env\n ? (globalThis as any).window.env\n : typeof process !== 'undefined'\n ? process.env\n : undefined\n const key = env?.GOOGLE_API_KEY || env?.GEMINI_API_KEY\n\n if (!key) {\n throw new Error(\n 'GOOGLE_API_KEY or GEMINI_API_KEY is required. Please set it in your environment variables or use the factory function with an explicit API key.',\n )\n }\n\n return key\n}\n\n/**\n * Generates a unique ID with a prefix\n */\nexport function generateId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`\n}\n"],"names":[],"mappings":";AASO,SAAS,mBAAmB,QAAyC;AAC1E,SAAO,IAAI,YAAY;AAAA,IACrB,QAAQ,OAAO;AAAA,EAAA,CAChB;AACH;AAMO,SAAS,yBAAiC;AAC/C,QAAM,MACJ,OAAO,eAAe,eAAgB,WAAmB,QAAQ,MAC5D,WAAmB,OAAO,MAC3B,OAAO,YAAY,cACjB,QAAQ,MACR;AACR,QAAM,MAAM,KAAK,kBAAkB,KAAK;AAExC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AAEA,SAAO;AACT;AAKO,SAAS,WAAW,QAAwB;AACjD,SAAO,GAAG,MAAM,IAAI,KAAK,KAAK,IAAI,KAAK,OAAA,EAAS,SAAS,EAAE,EAAE,UAAU,CAAC,CAAC;AAC3E;"}
1
+ {"version":3,"file":"client.js","sources":["../../../src/utils/client.ts"],"sourcesContent":["import { GoogleGenAI } from '@google/genai'\nimport type { GoogleGenAIOptions } from '@google/genai'\n\nexport interface GeminiClientConfig extends GoogleGenAIOptions {\n apiKey: string\n}\n\n/**\n * Creates a Google Generative AI client instance\n */\nexport function createGeminiClient(config: GeminiClientConfig): GoogleGenAI {\n return new GoogleGenAI({\n ...config,\n apiKey: config.apiKey,\n })\n}\n\n/**\n * Gets Google API key from environment variables\n * @throws Error if GOOGLE_API_KEY or GEMINI_API_KEY is not found\n */\nexport function getGeminiApiKeyFromEnv(): string {\n const env =\n typeof globalThis !== 'undefined' && (globalThis as any).window?.env\n ? (globalThis as any).window.env\n : typeof process !== 'undefined'\n ? process.env\n : undefined\n const key = env?.GOOGLE_API_KEY || env?.GEMINI_API_KEY\n\n if (!key) {\n throw new Error(\n 'GOOGLE_API_KEY or GEMINI_API_KEY is required. Please set it in your environment variables or use the factory function with an explicit API key.',\n )\n }\n\n return key\n}\n\n/**\n * Generates a unique ID with a prefix\n */\nexport function generateId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`\n}\n"],"names":[],"mappings":";AAUO,SAAS,mBAAmB,QAAyC;AAC1E,SAAO,IAAI,YAAY;AAAA,IACrB,GAAG;AAAA,IACH,QAAQ,OAAO;AAAA,EAAA,CAChB;AACH;AAMO,SAAS,yBAAiC;AAC/C,QAAM,MACJ,OAAO,eAAe,eAAgB,WAAmB,QAAQ,MAC5D,WAAmB,OAAO,MAC3B,OAAO,YAAY,cACjB,QAAQ,MACR;AACR,QAAM,MAAM,KAAK,kBAAkB,KAAK;AAExC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AAEA,SAAO;AACT;AAKO,SAAS,WAAW,QAAwB;AACjD,SAAO,GAAG,MAAM,IAAI,KAAK,KAAK,IAAI,KAAK,OAAA,EAAS,SAAS,EAAE,EAAE,UAAU,CAAC,CAAC;AAC3E;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-gemini",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "Google Gemini adapter for TanStack AI",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -33,12 +33,12 @@
33
33
  "@google/genai": "^1.30.0"
34
34
  },
35
35
  "peerDependencies": {
36
- "@tanstack/ai": "^0.2.2"
36
+ "@tanstack/ai": "^0.4.2"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@vitest/coverage-v8": "4.0.14",
40
40
  "vite": "^7.2.7",
41
- "@tanstack/ai": "0.2.2"
41
+ "@tanstack/ai": "0.4.2"
42
42
  },
43
43
  "scripts": {
44
44
  "build": "vite build",
@@ -4,8 +4,8 @@ import {
4
4
  generateId,
5
5
  getGeminiApiKeyFromEnv,
6
6
  } from '../utils'
7
-
8
7
  import type { GoogleGenAI } from '@google/genai'
8
+ import type { GeminiClientConfig } from '../utils'
9
9
  import type { SummarizeAdapter } from '@tanstack/ai/adapters'
10
10
  import type {
11
11
  StreamChunk,
@@ -13,6 +13,10 @@ import type {
13
13
  SummarizationResult,
14
14
  } from '@tanstack/ai'
15
15
 
16
+ /**
17
+ * Configuration for Gemini summarize adapter
18
+ */
19
+ export interface GeminiSummarizeConfig extends GeminiClientConfig {}
16
20
  /**
17
21
  * Available Gemini models for summarization
18
22
  */
@@ -66,15 +70,8 @@ export class GeminiSummarizeAdapter<
66
70
 
67
71
  private client: GoogleGenAI
68
72
 
69
- constructor(
70
- apiKeyOrClient: string | GoogleGenAI,
71
- model: TModel,
72
- _options: GeminiSummarizeAdapterOptions = {},
73
- ) {
74
- this.client =
75
- typeof apiKeyOrClient === 'string'
76
- ? createGeminiClient({ apiKey: apiKeyOrClient })
77
- : apiKeyOrClient
73
+ constructor(config: GeminiSummarizeConfig, model: TModel) {
74
+ this.client = createGeminiClient(config)
78
75
  this.model = model
79
76
  }
80
77
 
@@ -164,13 +161,12 @@ export class GeminiSummarizeAdapter<
164
161
  if (part.text) {
165
162
  accumulatedContent += part.text
166
163
  yield {
167
- type: 'content',
168
- id,
164
+ type: 'TEXT_MESSAGE_CONTENT',
165
+ messageId: id,
169
166
  model,
170
167
  timestamp: Date.now(),
171
168
  delta: part.text,
172
169
  content: accumulatedContent,
173
- role: 'assistant',
174
170
  }
175
171
  }
176
172
  }
@@ -184,8 +180,8 @@ export class GeminiSummarizeAdapter<
184
180
  finishReason === FinishReason.SAFETY
185
181
  ) {
186
182
  yield {
187
- type: 'done',
188
- id,
183
+ type: 'RUN_FINISHED',
184
+ runId: id,
189
185
  model,
190
186
  timestamp: Date.now(),
191
187
  finishReason:
@@ -225,9 +221,9 @@ export class GeminiSummarizeAdapter<
225
221
  export function createGeminiSummarize<TModel extends GeminiSummarizeModel>(
226
222
  apiKey: string,
227
223
  model: TModel,
228
- options?: GeminiSummarizeAdapterOptions,
224
+ config?: Omit<GeminiSummarizeConfig, 'apiKey'>,
229
225
  ): GeminiSummarizeAdapter<TModel> {
230
- return new GeminiSummarizeAdapter(apiKey, model, options)
226
+ return new GeminiSummarizeAdapter({ ...config, apiKey }, model)
231
227
  }
232
228
 
233
229
  /**
@@ -235,8 +231,8 @@ export function createGeminiSummarize<TModel extends GeminiSummarizeModel>(
235
231
  */
236
232
  export function geminiSummarize<TModel extends GeminiSummarizeModel>(
237
233
  model: TModel,
238
- options?: GeminiSummarizeAdapterOptions,
234
+ config?: Omit<GeminiSummarizeConfig, 'apiKey'>,
239
235
  ): GeminiSummarizeAdapter<TModel> {
240
236
  const apiKey = getGeminiApiKeyFromEnv()
241
- return new GeminiSummarizeAdapter(apiKey, model, options)
237
+ return new GeminiSummarizeAdapter({ ...config, apiKey }, model)
242
238
  }
@@ -30,13 +30,7 @@ import type {
30
30
  TextOptions,
31
31
  } from '@tanstack/ai'
32
32
  import type { ExternalTextProviderOptions } from '../text/text-provider-options'
33
- import type {
34
- GeminiAudioMetadata,
35
- GeminiDocumentMetadata,
36
- GeminiImageMetadata,
37
- GeminiMessageMetadataByModality,
38
- GeminiVideoMetadata,
39
- } from '../message-types'
33
+ import type { GeminiMessageMetadataByModality } from '../message-types'
40
34
  import type { GeminiClientConfig } from '../utils'
41
35
 
42
36
  /**
@@ -115,8 +109,7 @@ export class GeminiTextAdapter<
115
109
  } catch (error) {
116
110
  const timestamp = Date.now()
117
111
  yield {
118
- type: 'error',
119
- id: generateId(this.name),
112
+ type: 'RUN_ERROR',
120
113
  model: options.model,
121
114
  timestamp,
122
115
  error: {
@@ -203,35 +196,78 @@ export class GeminiTextAdapter<
203
196
  let accumulatedContent = ''
204
197
  const toolCallMap = new Map<
205
198
  string,
206
- { name: string; args: string; index: number }
199
+ { name: string; args: string; index: number; started: boolean }
207
200
  >()
208
201
  let nextToolIndex = 0
209
202
 
203
+ // AG-UI lifecycle tracking
204
+ const runId = generateId(this.name)
205
+ const messageId = generateId(this.name)
206
+ let stepId: string | null = null
207
+ let hasEmittedRunStarted = false
208
+ let hasEmittedTextMessageStart = false
209
+ let hasEmittedStepStarted = false
210
+
210
211
  for await (const chunk of result) {
212
+ // Emit RUN_STARTED on first chunk
213
+ if (!hasEmittedRunStarted) {
214
+ hasEmittedRunStarted = true
215
+ yield {
216
+ type: 'RUN_STARTED',
217
+ runId,
218
+ model,
219
+ timestamp,
220
+ }
221
+ }
222
+
211
223
  if (chunk.candidates?.[0]?.content?.parts) {
212
224
  const parts = chunk.candidates[0].content.parts
213
225
 
214
226
  for (const part of parts) {
215
227
  if (part.text) {
216
228
  if (part.thought) {
229
+ // Emit STEP_STARTED on first thinking content
230
+ if (!hasEmittedStepStarted) {
231
+ hasEmittedStepStarted = true
232
+ stepId = generateId(this.name)
233
+ yield {
234
+ type: 'STEP_STARTED',
235
+ stepId,
236
+ model,
237
+ timestamp,
238
+ stepType: 'thinking',
239
+ }
240
+ }
241
+
217
242
  yield {
218
- type: 'thinking',
219
- content: part.text,
220
- delta: part.text,
221
- id: generateId(this.name),
243
+ type: 'STEP_FINISHED',
244
+ stepId: stepId || generateId(this.name),
222
245
  model,
223
246
  timestamp,
247
+ delta: part.text,
248
+ content: part.text,
224
249
  }
225
250
  } else {
251
+ // Emit TEXT_MESSAGE_START on first text content
252
+ if (!hasEmittedTextMessageStart) {
253
+ hasEmittedTextMessageStart = true
254
+ yield {
255
+ type: 'TEXT_MESSAGE_START',
256
+ messageId,
257
+ model,
258
+ timestamp,
259
+ role: 'assistant',
260
+ }
261
+ }
262
+
226
263
  accumulatedContent += part.text
227
264
  yield {
228
- type: 'content',
229
- id: generateId(this.name),
265
+ type: 'TEXT_MESSAGE_CONTENT',
266
+ messageId,
230
267
  model,
231
268
  timestamp,
232
269
  delta: part.text,
233
270
  content: accumulatedContent,
234
- role: 'assistant',
235
271
  }
236
272
  }
237
273
  }
@@ -252,6 +288,7 @@ export class GeminiTextAdapter<
252
288
  ? functionArgs
253
289
  : JSON.stringify(functionArgs),
254
290
  index: nextToolIndex++,
291
+ started: false,
255
292
  }
256
293
  toolCallMap.set(toolCallId, toolCallData)
257
294
  } else {
@@ -271,33 +308,51 @@ export class GeminiTextAdapter<
271
308
  }
272
309
  }
273
310
 
311
+ // Emit TOOL_CALL_START if not already started
312
+ if (!toolCallData.started) {
313
+ toolCallData.started = true
314
+ yield {
315
+ type: 'TOOL_CALL_START',
316
+ toolCallId,
317
+ toolName: toolCallData.name,
318
+ model,
319
+ timestamp,
320
+ index: toolCallData.index,
321
+ }
322
+ }
323
+
324
+ // Emit TOOL_CALL_ARGS
274
325
  yield {
275
- type: 'tool_call',
276
- id: generateId(this.name),
326
+ type: 'TOOL_CALL_ARGS',
327
+ toolCallId,
277
328
  model,
278
329
  timestamp,
279
- toolCall: {
280
- id: toolCallId,
281
- type: 'function',
282
- function: {
283
- name: toolCallData.name,
284
- arguments: toolCallData.args,
285
- },
286
- },
287
- index: toolCallData.index,
330
+ delta: toolCallData.args,
331
+ args: toolCallData.args,
288
332
  }
289
333
  }
290
334
  }
291
335
  } else if (chunk.data) {
336
+ // Emit TEXT_MESSAGE_START on first text content
337
+ if (!hasEmittedTextMessageStart) {
338
+ hasEmittedTextMessageStart = true
339
+ yield {
340
+ type: 'TEXT_MESSAGE_START',
341
+ messageId,
342
+ model,
343
+ timestamp,
344
+ role: 'assistant',
345
+ }
346
+ }
347
+
292
348
  accumulatedContent += chunk.data
293
349
  yield {
294
- type: 'content',
295
- id: generateId(this.name),
350
+ type: 'TEXT_MESSAGE_CONTENT',
351
+ messageId,
296
352
  model,
297
353
  timestamp,
298
354
  delta: chunk.data,
299
355
  content: accumulatedContent,
300
- role: 'assistant',
301
356
  }
302
357
  }
303
358
 
@@ -314,53 +369,98 @@ export class GeminiTextAdapter<
314
369
  `${functionCall.name}_${Date.now()}_${nextToolIndex}`
315
370
  const functionArgs = functionCall.args || {}
316
371
 
372
+ const argsString =
373
+ typeof functionArgs === 'string'
374
+ ? functionArgs
375
+ : JSON.stringify(functionArgs)
376
+
317
377
  toolCallMap.set(toolCallId, {
318
378
  name: functionCall.name || '',
319
- args:
320
- typeof functionArgs === 'string'
321
- ? functionArgs
322
- : JSON.stringify(functionArgs),
379
+ args: argsString,
323
380
  index: nextToolIndex++,
381
+ started: true,
324
382
  })
325
383
 
384
+ // Emit TOOL_CALL_START
326
385
  yield {
327
- type: 'tool_call',
328
- id: generateId(this.name),
386
+ type: 'TOOL_CALL_START',
387
+ toolCallId,
388
+ toolName: functionCall.name || '',
329
389
  model,
330
390
  timestamp,
331
- toolCall: {
332
- id: toolCallId,
333
- type: 'function',
334
- function: {
335
- name: functionCall.name || '',
336
- arguments:
337
- typeof functionArgs === 'string'
338
- ? functionArgs
339
- : JSON.stringify(functionArgs),
340
- },
341
- },
342
391
  index: nextToolIndex - 1,
343
392
  }
393
+
394
+ // Emit TOOL_CALL_END with parsed input
395
+ let parsedInput: unknown = {}
396
+ try {
397
+ parsedInput =
398
+ typeof functionArgs === 'string'
399
+ ? JSON.parse(functionArgs)
400
+ : functionArgs
401
+ } catch {
402
+ parsedInput = {}
403
+ }
404
+
405
+ yield {
406
+ type: 'TOOL_CALL_END',
407
+ toolCallId,
408
+ toolName: functionCall.name || '',
409
+ model,
410
+ timestamp,
411
+ input: parsedInput,
412
+ }
344
413
  }
345
414
  }
346
415
  }
347
416
  }
417
+
418
+ // Emit TOOL_CALL_END for all tracked tool calls
419
+ for (const [toolCallId, toolCallData] of toolCallMap.entries()) {
420
+ let parsedInput: unknown = {}
421
+ try {
422
+ parsedInput = JSON.parse(toolCallData.args)
423
+ } catch {
424
+ parsedInput = {}
425
+ }
426
+
427
+ yield {
428
+ type: 'TOOL_CALL_END',
429
+ toolCallId,
430
+ toolName: toolCallData.name,
431
+ model,
432
+ timestamp,
433
+ input: parsedInput,
434
+ }
435
+ }
436
+
348
437
  if (finishReason === FinishReason.MAX_TOKENS) {
349
438
  yield {
350
- type: 'error',
351
- id: generateId(this.name),
439
+ type: 'RUN_ERROR',
440
+ runId,
352
441
  model,
353
442
  timestamp,
354
443
  error: {
355
444
  message:
356
445
  'The response was cut off because the maximum token limit was reached.',
446
+ code: 'max_tokens',
357
447
  },
358
448
  }
359
449
  }
360
450
 
451
+ // Emit TEXT_MESSAGE_END if we had text content
452
+ if (hasEmittedTextMessageStart) {
453
+ yield {
454
+ type: 'TEXT_MESSAGE_END',
455
+ messageId,
456
+ model,
457
+ timestamp,
458
+ }
459
+ }
460
+
361
461
  yield {
362
- type: 'done',
363
- id: generateId(this.name),
462
+ type: 'RUN_FINISHED',
463
+ runId,
364
464
  model,
365
465
  timestamp,
366
466
  finishReason: toolCallMap.size > 0 ? 'tool_calls' : 'stop',
@@ -377,20 +477,6 @@ export class GeminiTextAdapter<
377
477
  }
378
478
 
379
479
  private convertContentPartToGemini(part: ContentPart): Part {
380
- const getDefaultFileType = (
381
- part: 'image' | 'audio' | 'video' | 'document',
382
- ) => {
383
- switch (part) {
384
- case 'image':
385
- return 'image/jpeg'
386
- case 'audio':
387
- return 'audio/mp3'
388
- case 'video':
389
- return 'video/mp4'
390
- case 'document':
391
- return 'application/pdf'
392
- }
393
- }
394
480
  switch (part.type) {
395
481
  case 'text':
396
482
  return { text: part.content }
@@ -398,24 +484,26 @@ export class GeminiTextAdapter<
398
484
  case 'audio':
399
485
  case 'video':
400
486
  case 'document': {
401
- const metadata = part.metadata as
402
- | GeminiDocumentMetadata
403
- | GeminiImageMetadata
404
- | GeminiVideoMetadata
405
- | GeminiAudioMetadata
406
- | undefined
407
487
  if (part.source.type === 'data') {
408
488
  return {
409
489
  inlineData: {
410
490
  data: part.source.value,
411
- mimeType: metadata?.mimeType ?? getDefaultFileType(part.type),
491
+ mimeType: part.source.mimeType,
412
492
  },
413
493
  }
414
494
  } else {
495
+ // For URL sources, use provided mimeType or fall back to reasonable defaults
496
+ const defaultMimeType = {
497
+ image: 'image/jpeg',
498
+ audio: 'audio/mp3',
499
+ video: 'video/mp4',
500
+ document: 'application/pdf',
501
+ }[part.type]
502
+
415
503
  return {
416
504
  fileData: {
417
505
  fileUri: part.source.value,
418
- mimeType: metadata?.mimeType ?? getDefaultFileType(part.type),
506
+ mimeType: part.source.mimeType ?? defaultMimeType,
419
507
  },
420
508
  }
421
509
  }
package/src/index.ts CHANGED
@@ -79,9 +79,5 @@ export type {
79
79
  GeminiAudioMetadata,
80
80
  GeminiVideoMetadata,
81
81
  GeminiDocumentMetadata,
82
- GeminiImageMimeType,
83
- GeminiAudioMimeType,
84
- GeminiVideoMimeType,
85
- GeminiDocumentMimeType,
86
82
  GeminiMessageMetadataByModality,
87
83
  } from './message-types'
@@ -1,6 +1,7 @@
1
1
  import { GoogleGenAI } from '@google/genai'
2
+ import type { GoogleGenAIOptions } from '@google/genai'
2
3
 
3
- export interface GeminiClientConfig {
4
+ export interface GeminiClientConfig extends GoogleGenAIOptions {
4
5
  apiKey: string
5
6
  }
6
7
 
@@ -9,6 +10,7 @@ export interface GeminiClientConfig {
9
10
  */
10
11
  export function createGeminiClient(config: GeminiClientConfig): GoogleGenAI {
11
12
  return new GoogleGenAI({
13
+ ...config,
12
14
  apiKey: config.apiKey,
13
15
  })
14
16
  }