@tanstack/ai-gemini 0.20.0 → 0.21.0

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.
Files changed (62) hide show
  1. package/README.md +15 -1
  2. package/dist/esm/adapters/audio.js +113 -61
  3. package/dist/esm/adapters/audio.js.map +1 -1
  4. package/dist/esm/adapters/image.js +224 -225
  5. package/dist/esm/adapters/image.js.map +1 -1
  6. package/dist/esm/adapters/summarize.js +33 -12
  7. package/dist/esm/adapters/summarize.js.map +1 -1
  8. package/dist/esm/adapters/text.js +574 -650
  9. package/dist/esm/adapters/text.js.map +1 -1
  10. package/dist/esm/adapters/tts.js +197 -178
  11. package/dist/esm/adapters/tts.js.map +1 -1
  12. package/dist/esm/adapters/video.js +456 -409
  13. package/dist/esm/adapters/video.js.map +1 -1
  14. package/dist/esm/experimental/index.js +1 -6
  15. package/dist/esm/experimental/text-interactions/adapter.js +877 -1014
  16. package/dist/esm/experimental/text-interactions/adapter.js.map +1 -1
  17. package/dist/esm/image/image-provider-options.js +74 -54
  18. package/dist/esm/image/image-provider-options.js.map +1 -1
  19. package/dist/esm/index.js +4 -36
  20. package/dist/esm/model-meta.d.ts +53 -5
  21. package/dist/esm/model-meta.js +733 -129
  22. package/dist/esm/model-meta.js.map +1 -1
  23. package/dist/esm/realtime/adapter.js +242 -228
  24. package/dist/esm/realtime/adapter.js.map +1 -1
  25. package/dist/esm/realtime/client.js +314 -381
  26. package/dist/esm/realtime/client.js.map +1 -1
  27. package/dist/esm/realtime/index.js +3 -0
  28. package/dist/esm/realtime/token.js +50 -36
  29. package/dist/esm/realtime/token.js.map +1 -1
  30. package/dist/esm/realtime/utils.js +202 -245
  31. package/dist/esm/realtime/utils.js.map +1 -1
  32. package/dist/esm/tools/code-execution-tool.js +11 -13
  33. package/dist/esm/tools/code-execution-tool.js.map +1 -1
  34. package/dist/esm/tools/computer-use-tool.js +18 -28
  35. package/dist/esm/tools/computer-use-tool.js.map +1 -1
  36. package/dist/esm/tools/file-search-tool.js +11 -14
  37. package/dist/esm/tools/file-search-tool.js.map +1 -1
  38. package/dist/esm/tools/function-declaration-tool.js +11 -25
  39. package/dist/esm/tools/function-declaration-tool.js.map +1 -1
  40. package/dist/esm/tools/google-maps-tool.js +11 -14
  41. package/dist/esm/tools/google-maps-tool.js.map +1 -1
  42. package/dist/esm/tools/google-search-retriveal-tool.js +11 -14
  43. package/dist/esm/tools/google-search-retriveal-tool.js.map +1 -1
  44. package/dist/esm/tools/google-search-tool.js +11 -14
  45. package/dist/esm/tools/google-search-tool.js.map +1 -1
  46. package/dist/esm/tools/index.js +2 -13
  47. package/dist/esm/tools/tool-converter.js +64 -57
  48. package/dist/esm/tools/tool-converter.js.map +1 -1
  49. package/dist/esm/tools/url-context-tool.js +11 -13
  50. package/dist/esm/tools/url-context-tool.js.map +1 -1
  51. package/dist/esm/usage.js +81 -87
  52. package/dist/esm/usage.js.map +1 -1
  53. package/dist/esm/utils/client.js +31 -24
  54. package/dist/esm/utils/client.js.map +1 -1
  55. package/dist/esm/utils/index.js +2 -0
  56. package/dist/esm/video/video-provider-options.js +73 -20
  57. package/dist/esm/video/video-provider-options.js.map +1 -1
  58. package/package.json +7 -7
  59. package/src/model-meta.ts +120 -10
  60. package/dist/esm/experimental/index.js.map +0 -1
  61. package/dist/esm/index.js.map +0 -1
  62. package/dist/esm/tools/index.js.map +0 -1
@@ -1,1052 +1,915 @@
1
+ import { createGeminiClient, generateId, getGeminiApiKeyFromEnv } from "../../utils/client.js";
1
2
  import { EventType } from "@tanstack/ai";
2
3
  import { BaseTextAdapter } from "@tanstack/ai/adapters";
3
4
  import { parse } from "partial-json";
4
- import { createGeminiClient, generateId, getGeminiApiKeyFromEnv } from "../../utils/client.js";
5
- class GeminiTextInteractionsAdapter extends BaseTextAdapter {
6
- kind = "text";
7
- name = "gemini-text-interactions";
8
- client;
9
- // Tracks the most recent server-assigned interaction id per threadId
10
- // so the adapter can chain follow-up calls on the same thread without
11
- // the caller having to thread the id manually. Two callers rely on
12
- // this:
13
- // 1. The agent loop's tool-call iterations (each iteration is a new
14
- // `chatStream` call with accumulated tool messages).
15
- // 2. The agentic-structured composition: a `chatStream` run followed
16
- // by `structuredOutput` on the accumulated messages.
17
- // Cross-request chaining is the caller's job via
18
- // `modelOptions.previous_interaction_id`. To keep stale ids from
19
- // chaining a brand-new turn, `chatStream` evicts at the START when
20
- // the caller signals fresh-turn intent (no caller-provided id AND a
21
- // single user message — anything else is a follow-up). Errors evict
22
- // immediately so a failed turn never chains into the next one.
23
- interactionIdByThread = /* @__PURE__ */ new Map();
24
- constructor(config, model) {
25
- super({}, model);
26
- this.client = createGeminiClient(config);
27
- }
28
- async *chatStream(options) {
29
- const runId = options.runId ?? generateId(this.name);
30
- const threadId = options.threadId ?? generateId(this.name);
31
- const timestamp = Date.now();
32
- const { logger } = options;
33
- if (!options.modelOptions?.previous_interaction_id && options.messages.length === 1 && options.messages[0]?.role === "user") {
34
- this.interactionIdByThread.delete(threadId);
35
- }
36
- const effectivePreviousInteractionId = options.modelOptions?.previous_interaction_id ?? this.interactionIdByThread.get(threadId);
37
- let sawTerminalEvent = false;
38
- let completedTryBlock = false;
39
- try {
40
- const request = buildInteractionsRequest({
41
- ...options,
42
- modelOptions: {
43
- ...options.modelOptions,
44
- previous_interaction_id: effectivePreviousInteractionId
45
- }
46
- });
47
- logger.request(
48
- `activity=chat provider=gemini-text-interactions model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`,
49
- {
50
- provider: "gemini-text-interactions",
51
- model: this.model,
52
- request
53
- }
54
- );
55
- const stream = await this.client.interactions.create(
56
- { ...request, stream: true },
57
- { signal: options.abortController?.signal }
58
- );
59
- for await (const chunk of translateInteractionEvents(
60
- stream,
61
- options.model,
62
- runId,
63
- threadId,
64
- options.parentRunId,
65
- timestamp,
66
- this.name,
67
- logger
68
- )) {
69
- if (chunk.type === EventType.CUSTOM && chunk.name === "gemini.interactionId") {
70
- const value = chunk.value;
71
- this.interactionIdByThread.set(threadId, value.interactionId);
72
- }
73
- if (chunk.type === EventType.RUN_FINISHED) {
74
- sawTerminalEvent = true;
75
- } else if (chunk.type === EventType.RUN_ERROR) {
76
- sawTerminalEvent = true;
77
- this.interactionIdByThread.delete(threadId);
78
- }
79
- yield chunk;
80
- }
81
- if (!sawTerminalEvent) {
82
- this.interactionIdByThread.delete(threadId);
83
- const message = "Gemini Interactions stream ended without a terminal event (no interaction.complete or error)";
84
- logger.errors("gemini-text-interactions.chatStream truncated", {
85
- source: "gemini-text-interactions.chatStream",
86
- runId,
87
- threadId
88
- });
89
- yield {
90
- type: EventType.RUN_ERROR,
91
- runId,
92
- model: options.model,
93
- timestamp,
94
- message,
95
- error: { message }
96
- };
97
- }
98
- completedTryBlock = true;
99
- } catch (error) {
100
- this.interactionIdByThread.delete(threadId);
101
- const message = error instanceof Error ? error.message : "An unknown error occurred during the interactions stream.";
102
- logger.errors("gemini-text-interactions.chatStream fatal", {
103
- error,
104
- source: "gemini-text-interactions.chatStream"
105
- });
106
- yield {
107
- type: EventType.RUN_ERROR,
108
- runId,
109
- model: options.model,
110
- timestamp,
111
- message,
112
- error: { message }
113
- };
114
- } finally {
115
- if (!completedTryBlock) {
116
- this.interactionIdByThread.delete(threadId);
117
- }
118
- }
119
- }
120
- async structuredOutput(options) {
121
- const { chatOptions, outputSchema } = options;
122
- const { logger } = chatOptions;
123
- const threadId = chatOptions.threadId;
124
- const effectivePreviousInteractionId = chatOptions.modelOptions?.previous_interaction_id ?? (threadId ? this.interactionIdByThread.get(threadId) : void 0);
125
- const baseRequest = buildInteractionsRequest({
126
- ...chatOptions,
127
- modelOptions: {
128
- ...chatOptions.modelOptions,
129
- previous_interaction_id: effectivePreviousInteractionId
130
- }
131
- });
132
- const request = {
133
- ...baseRequest,
134
- response_format: {
135
- type: "text",
136
- mime_type: "application/json",
137
- schema: outputSchema
138
- }
139
- };
140
- try {
141
- logger.request(
142
- `activity=chat provider=gemini-text-interactions model=${this.model} messages=${chatOptions.messages.length} tools=${chatOptions.tools?.length ?? 0} stream=false`,
143
- {
144
- provider: "gemini-text-interactions",
145
- model: this.model,
146
- request
147
- }
148
- );
149
- const result = await this.client.interactions.create(
150
- request,
151
- { signal: chatOptions.abortController?.signal }
152
- );
153
- const rawText = extractTextFromInteraction(result);
154
- if (!rawText) {
155
- throw new Error(
156
- `Gemini Interactions returned no text output for structured-output request (status: ${result.status}). The model may have produced only tool calls or non-text content.`
157
- );
158
- }
159
- let parsed;
160
- try {
161
- parsed = JSON.parse(rawText);
162
- } catch {
163
- throw new Error(
164
- `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? "..." : ""}`
165
- );
166
- }
167
- return { data: parsed, rawText };
168
- } catch (error) {
169
- logger.errors("gemini-text-interactions.structuredOutput fatal", {
170
- error,
171
- source: "gemini-text-interactions.structuredOutput"
172
- });
173
- throw new Error(
174
- error instanceof Error ? error.message : "An unknown error occurred during structured output generation.",
175
- { cause: error }
176
- );
177
- }
178
- }
179
- }
5
+ //#region src/experimental/text-interactions/adapter.ts
6
+ /**
7
+ * Tree-shakeable adapter for Gemini's stateful Interactions API. Routes
8
+ * through `client.interactions.create` and surfaces the server-assigned
9
+ * `interactionId` via an AG-UI `CUSTOM` event with
10
+ * `name: 'gemini.interactionId'` emitted just before `RUN_FINISHED`; pass
11
+ * that id back on the next turn via `modelOptions.previous_interaction_id`
12
+ * to continue the conversation without resending history.
13
+ *
14
+ * The Interactions API does NOT support stateless multi-turn replay —
15
+ * passing more than one message in `messages` without a
16
+ * `previous_interaction_id` throws. For a chat UI that maintains local
17
+ * history (e.g. `useChat`), see the "Wiring with `useChat`" section of
18
+ * `docs/adapters/gemini.md` for the canonical client/server pattern.
19
+ *
20
+ * Supports user-defined function tools and the built-in tools
21
+ * `google_search`, `code_execution`, `url_context`, `file_search`, and
22
+ * `computer_use`. Built-in tool *activity* for the four search/exec
23
+ * variants is surfaced via `CUSTOM` events
24
+ * (`gemini.googleSearchCall` / `gemini.googleSearchResult` and the
25
+ * corresponding per-tool variants) carrying the raw Interactions delta;
26
+ * see {@link GeminiInteractionsCustomEvent}. `computer_use` is accepted
27
+ * in the request but the Interactions API does not currently stream
28
+ * per-delta CUSTOM events for it. `google_search_retrieval`,
29
+ * `google_maps`, and `mcp_server` are not supported on this adapter.
30
+ *
31
+ * @experimental Interactions API is in Beta per Google; shapes may change.
32
+ * @see https://ai.google.dev/gemini-api/docs/interactions
33
+ */
34
+ var GeminiTextInteractionsAdapter = class extends BaseTextAdapter {
35
+ kind = "text";
36
+ name = "gemini-text-interactions";
37
+ client;
38
+ interactionIdByThread = /* @__PURE__ */ new Map();
39
+ constructor(config, model) {
40
+ super({}, model);
41
+ this.client = createGeminiClient(config);
42
+ }
43
+ async *chatStream(options) {
44
+ const runId = options.runId ?? generateId(this.name);
45
+ const threadId = options.threadId ?? generateId(this.name);
46
+ const timestamp = Date.now();
47
+ const { logger } = options;
48
+ if (!options.modelOptions?.previous_interaction_id && options.messages.length === 1 && options.messages[0]?.role === "user") this.interactionIdByThread.delete(threadId);
49
+ const effectivePreviousInteractionId = options.modelOptions?.previous_interaction_id ?? this.interactionIdByThread.get(threadId);
50
+ let sawTerminalEvent = false;
51
+ let completedTryBlock = false;
52
+ try {
53
+ const request = buildInteractionsRequest({
54
+ ...options,
55
+ modelOptions: {
56
+ ...options.modelOptions,
57
+ previous_interaction_id: effectivePreviousInteractionId
58
+ }
59
+ });
60
+ logger.request(`activity=chat provider=gemini-text-interactions model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`, {
61
+ provider: "gemini-text-interactions",
62
+ model: this.model,
63
+ request
64
+ });
65
+ const stream = await this.client.interactions.create({
66
+ ...request,
67
+ stream: true
68
+ }, { signal: options.abortController?.signal });
69
+ for await (const chunk of translateInteractionEvents(stream, options.model, runId, threadId, options.parentRunId, timestamp, this.name, logger)) {
70
+ if (chunk.type === EventType.CUSTOM && chunk.name === "gemini.interactionId") {
71
+ const value = chunk.value;
72
+ this.interactionIdByThread.set(threadId, value.interactionId);
73
+ }
74
+ if (chunk.type === EventType.RUN_FINISHED) sawTerminalEvent = true;
75
+ else if (chunk.type === EventType.RUN_ERROR) {
76
+ sawTerminalEvent = true;
77
+ this.interactionIdByThread.delete(threadId);
78
+ }
79
+ yield chunk;
80
+ }
81
+ if (!sawTerminalEvent) {
82
+ this.interactionIdByThread.delete(threadId);
83
+ const message = "Gemini Interactions stream ended without a terminal event (no interaction.complete or error)";
84
+ logger.errors("gemini-text-interactions.chatStream truncated", {
85
+ source: "gemini-text-interactions.chatStream",
86
+ runId,
87
+ threadId
88
+ });
89
+ yield {
90
+ type: EventType.RUN_ERROR,
91
+ runId,
92
+ model: options.model,
93
+ timestamp,
94
+ message,
95
+ error: { message }
96
+ };
97
+ }
98
+ completedTryBlock = true;
99
+ } catch (error) {
100
+ this.interactionIdByThread.delete(threadId);
101
+ const message = error instanceof Error ? error.message : "An unknown error occurred during the interactions stream.";
102
+ logger.errors("gemini-text-interactions.chatStream fatal", {
103
+ error,
104
+ source: "gemini-text-interactions.chatStream"
105
+ });
106
+ yield {
107
+ type: EventType.RUN_ERROR,
108
+ runId,
109
+ model: options.model,
110
+ timestamp,
111
+ message,
112
+ error: { message }
113
+ };
114
+ } finally {
115
+ if (!completedTryBlock) this.interactionIdByThread.delete(threadId);
116
+ }
117
+ }
118
+ async structuredOutput(options) {
119
+ const { chatOptions, outputSchema } = options;
120
+ const { logger } = chatOptions;
121
+ const threadId = chatOptions.threadId;
122
+ const effectivePreviousInteractionId = chatOptions.modelOptions?.previous_interaction_id ?? (threadId ? this.interactionIdByThread.get(threadId) : void 0);
123
+ const request = {
124
+ ...buildInteractionsRequest({
125
+ ...chatOptions,
126
+ modelOptions: {
127
+ ...chatOptions.modelOptions,
128
+ previous_interaction_id: effectivePreviousInteractionId
129
+ }
130
+ }),
131
+ response_format: {
132
+ type: "text",
133
+ mime_type: "application/json",
134
+ schema: outputSchema
135
+ }
136
+ };
137
+ try {
138
+ logger.request(`activity=chat provider=gemini-text-interactions model=${this.model} messages=${chatOptions.messages.length} tools=${chatOptions.tools?.length ?? 0} stream=false`, {
139
+ provider: "gemini-text-interactions",
140
+ model: this.model,
141
+ request
142
+ });
143
+ const result = await this.client.interactions.create(request, { signal: chatOptions.abortController?.signal });
144
+ const rawText = extractTextFromInteraction(result);
145
+ if (!rawText) throw new Error(`Gemini Interactions returned no text output for structured-output request (status: ${result.status}). The model may have produced only tool calls or non-text content.`);
146
+ let parsed;
147
+ try {
148
+ parsed = JSON.parse(rawText);
149
+ } catch {
150
+ throw new Error(`Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? "..." : ""}`);
151
+ }
152
+ return {
153
+ data: parsed,
154
+ rawText
155
+ };
156
+ } catch (error) {
157
+ logger.errors("gemini-text-interactions.structuredOutput fatal", {
158
+ error,
159
+ source: "gemini-text-interactions.structuredOutput"
160
+ });
161
+ throw new Error(error instanceof Error ? error.message : "An unknown error occurred during structured output generation.", { cause: error });
162
+ }
163
+ }
164
+ };
165
+ /** @experimental Interactions API is in Beta. */
180
166
  function createGeminiTextInteractions(model, apiKey, config) {
181
- return new GeminiTextInteractionsAdapter({ apiKey, ...config }, model);
167
+ return new GeminiTextInteractionsAdapter({
168
+ apiKey,
169
+ ...config
170
+ }, model);
182
171
  }
172
+ /** @experimental Interactions API is in Beta. */
183
173
  function geminiTextInteractions(model, config) {
184
- const apiKey = getGeminiApiKeyFromEnv();
185
- return createGeminiTextInteractions(model, apiKey, config);
174
+ return createGeminiTextInteractions(model, getGeminiApiKeyFromEnv(), config);
186
175
  }
187
176
  function buildInteractionsRequest(options) {
188
- const modelOpts = options.modelOptions;
189
- const systemInstruction = modelOpts?.system_instruction ?? options.systemPrompts?.join("\n");
190
- const generationConfig = {
191
- ...modelOpts?.generation_config
192
- };
193
- const hasGenerationConfig = Object.keys(generationConfig).length > 0;
194
- const input = convertMessagesToInteractionsInput(
195
- options.messages,
196
- modelOpts?.previous_interaction_id !== void 0
197
- );
198
- return {
199
- model: options.model,
200
- input,
201
- previous_interaction_id: modelOpts?.previous_interaction_id,
202
- system_instruction: systemInstruction,
203
- tools: convertToolsToInteractionsFormat(options.tools),
204
- generation_config: hasGenerationConfig ? generationConfig : void 0,
205
- store: modelOpts?.store,
206
- background: modelOpts?.background,
207
- response_modalities: modelOpts?.response_modalities,
208
- response_format: modelOpts?.response_format
209
- };
177
+ const modelOpts = options.modelOptions;
178
+ const systemInstruction = modelOpts?.system_instruction ?? options.systemPrompts?.join("\n");
179
+ const generationConfig = { ...modelOpts?.generation_config };
180
+ const hasGenerationConfig = Object.keys(generationConfig).length > 0;
181
+ const input = convertMessagesToInteractionsInput(options.messages, modelOpts?.previous_interaction_id !== void 0);
182
+ return {
183
+ model: options.model,
184
+ input,
185
+ previous_interaction_id: modelOpts?.previous_interaction_id,
186
+ system_instruction: systemInstruction,
187
+ tools: convertToolsToInteractionsFormat(options.tools),
188
+ generation_config: hasGenerationConfig ? generationConfig : void 0,
189
+ store: modelOpts?.store,
190
+ background: modelOpts?.background,
191
+ response_modalities: modelOpts?.response_modalities,
192
+ response_format: modelOpts?.response_format
193
+ };
210
194
  }
211
195
  function convertMessagesToInteractionsInput(messages, hasPreviousInteraction) {
212
- const toolCallIdToName = /* @__PURE__ */ new Map();
213
- for (const msg of messages) {
214
- if (msg.role === "assistant" && msg.toolCalls) {
215
- for (const tc of msg.toolCalls) {
216
- toolCallIdToName.set(tc.id, tc.function.name);
217
- }
218
- }
219
- }
220
- const source = hasPreviousInteraction ? messagesAfterLastAssistant(messages) : messages;
221
- if (hasPreviousInteraction && source.length === 0) {
222
- throw new Error(
223
- "Gemini Interactions adapter: modelOptions.previous_interaction_id was provided but no new messages were found after the last assistant turn. Append at least one user or tool message before chaining."
224
- );
225
- }
226
- if (!hasPreviousInteraction) {
227
- const [only, ...rest] = source;
228
- if (!only) {
229
- throw new Error("Gemini Interactions adapter: no messages to send.");
230
- }
231
- if (rest.length > 0) {
232
- throw new Error(
233
- 'Gemini Interactions adapter: cannot send prior conversation history on a fresh interaction. Either set modelOptions.previous_interaction_id to chain prior turns server-side, or trim the message list to a single new user turn. See docs/adapters/gemini.md ("Wiring with useChat") for the canonical client/server pattern.'
234
- );
235
- }
236
- if (only.role !== "user") {
237
- throw new Error(
238
- `Gemini Interactions adapter: the first message of a fresh interaction must be a user turn (got role="${only.role}"). Set modelOptions.previous_interaction_id to continue an existing interaction.`
239
- );
240
- }
241
- const content = messageToContentBlocks(only);
242
- if (content.length === 0) {
243
- throw new Error(
244
- "Gemini Interactions adapter: the user message produced no content blocks to send."
245
- );
246
- }
247
- return [{ type: "user_input", content }];
248
- }
249
- const steps = [];
250
- for (const msg of source) {
251
- if (msg.role === "tool" && msg.toolCallId) {
252
- const result = serializeToolResultContent(msg.content);
253
- steps.push({
254
- type: "function_result",
255
- call_id: msg.toolCallId,
256
- name: toolCallIdToName.get(msg.toolCallId),
257
- result
258
- });
259
- } else if (msg.role === "user") {
260
- const content = messageToContentBlocks(msg);
261
- if (content.length > 0) {
262
- steps.push({ type: "user_input", content });
263
- }
264
- }
265
- }
266
- if (steps.length === 0) {
267
- throw new Error(
268
- "Gemini Interactions adapter: messages after the last assistant turn produced no steps to send."
269
- );
270
- }
271
- return steps;
196
+ const toolCallIdToName = /* @__PURE__ */ new Map();
197
+ for (const msg of messages) if (msg.role === "assistant" && msg.toolCalls) for (const tc of msg.toolCalls) toolCallIdToName.set(tc.id, tc.function.name);
198
+ const source = hasPreviousInteraction ? messagesAfterLastAssistant(messages) : messages;
199
+ if (hasPreviousInteraction && source.length === 0) throw new Error("Gemini Interactions adapter: modelOptions.previous_interaction_id was provided but no new messages were found after the last assistant turn. Append at least one user or tool message before chaining.");
200
+ if (!hasPreviousInteraction) {
201
+ const [only, ...rest] = source;
202
+ if (!only) throw new Error("Gemini Interactions adapter: no messages to send.");
203
+ if (rest.length > 0) throw new Error("Gemini Interactions adapter: cannot send prior conversation history on a fresh interaction. Either set modelOptions.previous_interaction_id to chain prior turns server-side, or trim the message list to a single new user turn. See docs/adapters/gemini.md (\"Wiring with useChat\") for the canonical client/server pattern.");
204
+ if (only.role !== "user") throw new Error(`Gemini Interactions adapter: the first message of a fresh interaction must be a user turn (got role="${only.role}"). Set modelOptions.previous_interaction_id to continue an existing interaction.`);
205
+ const content = messageToContentBlocks(only);
206
+ if (content.length === 0) throw new Error("Gemini Interactions adapter: the user message produced no content blocks to send.");
207
+ return [{
208
+ type: "user_input",
209
+ content
210
+ }];
211
+ }
212
+ const steps = [];
213
+ for (const msg of source) if (msg.role === "tool" && msg.toolCallId) {
214
+ const result = serializeToolResultContent(msg.content);
215
+ steps.push({
216
+ type: "function_result",
217
+ call_id: msg.toolCallId,
218
+ name: toolCallIdToName.get(msg.toolCallId),
219
+ result
220
+ });
221
+ } else if (msg.role === "user") {
222
+ const content = messageToContentBlocks(msg);
223
+ if (content.length > 0) steps.push({
224
+ type: "user_input",
225
+ content
226
+ });
227
+ }
228
+ if (steps.length === 0) throw new Error("Gemini Interactions adapter: messages after the last assistant turn produced no steps to send.");
229
+ return steps;
272
230
  }
273
231
  function serializeToolResultContent(content) {
274
- if (typeof content === "string") return content;
275
- if (content === null || content === void 0) {
276
- throw new Error(
277
- "Gemini Interactions adapter: tool message has no content. The Interactions API requires a string `result` on function_result steps — return a string from your tool implementation (encode JSON/multimodal output yourself)."
278
- );
279
- }
280
- throw new Error(
281
- "Gemini Interactions adapter: tool message content must be a string (got an array of content parts). The Interactions API requires a string `result` on function_result steps — stringify multimodal tool output before returning it from your tool."
282
- );
232
+ if (typeof content === "string") return content;
233
+ if (content === null || content === void 0) throw new Error("Gemini Interactions adapter: tool message has no content. The Interactions API requires a string `result` on function_result steps — return a string from your tool implementation (encode JSON/multimodal output yourself).");
234
+ throw new Error("Gemini Interactions adapter: tool message content must be a string (got an array of content parts). The Interactions API requires a string `result` on function_result steps — stringify multimodal tool output before returning it from your tool.");
283
235
  }
284
236
  function messageToContentBlocks(msg) {
285
- const blocks = [];
286
- if (Array.isArray(msg.content)) {
287
- for (const part of msg.content) {
288
- blocks.push(contentPartToBlock(part));
289
- }
290
- } else if (typeof msg.content === "string" && msg.content && msg.role !== "tool") {
291
- blocks.push({ type: "text", text: msg.content });
292
- }
293
- return blocks;
237
+ const blocks = [];
238
+ if (Array.isArray(msg.content)) for (const part of msg.content) blocks.push(contentPartToBlock(part));
239
+ else if (typeof msg.content === "string" && msg.content && msg.role !== "tool") blocks.push({
240
+ type: "text",
241
+ text: msg.content
242
+ });
243
+ return blocks;
294
244
  }
295
245
  function messagesAfterLastAssistant(messages) {
296
- for (let i = messages.length - 1; i >= 0; i--) {
297
- if (messages[i]?.role === "assistant") {
298
- return messages.slice(i + 1);
299
- }
300
- }
301
- return messages;
246
+ for (let i = messages.length - 1; i >= 0; i--) if (messages[i]?.role === "assistant") return messages.slice(i + 1);
247
+ return messages;
302
248
  }
303
249
  function parsePartialToolArguments(raw) {
304
- if (!raw) return void 0;
305
- try {
306
- const parsed = parse(raw);
307
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
308
- } catch {
309
- return void 0;
310
- }
250
+ if (!raw) return void 0;
251
+ try {
252
+ const parsed = parse(raw);
253
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
254
+ } catch {
255
+ return;
256
+ }
311
257
  }
312
- const IMAGE_MIME_TYPES = [
313
- "image/png",
314
- "image/jpeg",
315
- "image/webp",
316
- "image/heic",
317
- "image/heif"
318
- ];
319
- const AUDIO_MIME_TYPES = [
320
- "audio/wav",
321
- "audio/mp3",
322
- "audio/aiff",
323
- "audio/aac",
324
- "audio/ogg",
325
- "audio/flac"
258
+ var IMAGE_MIME_TYPES = [
259
+ "image/png",
260
+ "image/jpeg",
261
+ "image/webp",
262
+ "image/heic",
263
+ "image/heif"
326
264
  ];
327
- const VIDEO_MIME_TYPES = [
328
- "video/mp4",
329
- "video/mpeg",
330
- "video/mpg",
331
- "video/mov",
332
- "video/avi",
333
- "video/x-flv",
334
- "video/webm",
335
- "video/wmv",
336
- "video/3gpp"
265
+ var AUDIO_MIME_TYPES = [
266
+ "audio/wav",
267
+ "audio/mp3",
268
+ "audio/aiff",
269
+ "audio/aac",
270
+ "audio/ogg",
271
+ "audio/flac"
337
272
  ];
338
- const DOCUMENT_MIME_TYPES = [
339
- "application/pdf"
273
+ var VIDEO_MIME_TYPES = [
274
+ "video/mp4",
275
+ "video/mpeg",
276
+ "video/mpg",
277
+ "video/mov",
278
+ "video/avi",
279
+ "video/x-flv",
280
+ "video/webm",
281
+ "video/wmv",
282
+ "video/3gpp"
340
283
  ];
284
+ var DOCUMENT_MIME_TYPES = ["application/pdf"];
341
285
  function validateMime(allowed, value, kind) {
342
- if (value === void 0) return void 0;
343
- if (allowed.includes(value)) {
344
- return value;
345
- }
346
- throw new Error(
347
- `Unsupported ${kind} mime type "${value}" for the Gemini Interactions API. Allowed: ${allowed.join(", ")}.`
348
- );
286
+ if (value === void 0) return void 0;
287
+ if (allowed.includes(value)) return value;
288
+ throw new Error(`Unsupported ${kind} mime type "${value}" for the Gemini Interactions API. Allowed: ${allowed.join(", ")}.`);
349
289
  }
350
290
  function contentPartToBlock(part) {
351
- if (part.type === "text") {
352
- return { type: "text", text: part.content };
353
- }
354
- const isData = part.source.type === "data";
355
- switch (part.type) {
356
- case "image": {
357
- const mime_type = validateMime(
358
- IMAGE_MIME_TYPES,
359
- part.source.mimeType,
360
- "image"
361
- );
362
- return isData ? { type: "image", data: part.source.value, mime_type } : { type: "image", uri: part.source.value, mime_type };
363
- }
364
- case "audio": {
365
- const mime_type = validateMime(
366
- AUDIO_MIME_TYPES,
367
- part.source.mimeType,
368
- "audio"
369
- );
370
- return isData ? { type: "audio", data: part.source.value, mime_type } : { type: "audio", uri: part.source.value, mime_type };
371
- }
372
- case "video": {
373
- const mime_type = validateMime(
374
- VIDEO_MIME_TYPES,
375
- part.source.mimeType,
376
- "video"
377
- );
378
- return isData ? { type: "video", data: part.source.value, mime_type } : { type: "video", uri: part.source.value, mime_type };
379
- }
380
- case "document": {
381
- const mime_type = validateMime(
382
- DOCUMENT_MIME_TYPES,
383
- part.source.mimeType,
384
- "document"
385
- );
386
- return isData ? { type: "document", data: part.source.value, mime_type } : { type: "document", uri: part.source.value, mime_type };
387
- }
388
- }
291
+ if (part.type === "text") return {
292
+ type: "text",
293
+ text: part.content
294
+ };
295
+ const isData = part.source.type === "data";
296
+ switch (part.type) {
297
+ case "image": {
298
+ const mime_type = validateMime(IMAGE_MIME_TYPES, part.source.mimeType, "image");
299
+ return isData ? {
300
+ type: "image",
301
+ data: part.source.value,
302
+ mime_type
303
+ } : {
304
+ type: "image",
305
+ uri: part.source.value,
306
+ mime_type
307
+ };
308
+ }
309
+ case "audio": {
310
+ const mime_type = validateMime(AUDIO_MIME_TYPES, part.source.mimeType, "audio");
311
+ return isData ? {
312
+ type: "audio",
313
+ data: part.source.value,
314
+ mime_type
315
+ } : {
316
+ type: "audio",
317
+ uri: part.source.value,
318
+ mime_type
319
+ };
320
+ }
321
+ case "video": {
322
+ const mime_type = validateMime(VIDEO_MIME_TYPES, part.source.mimeType, "video");
323
+ return isData ? {
324
+ type: "video",
325
+ data: part.source.value,
326
+ mime_type
327
+ } : {
328
+ type: "video",
329
+ uri: part.source.value,
330
+ mime_type
331
+ };
332
+ }
333
+ case "document": {
334
+ const mime_type = validateMime(DOCUMENT_MIME_TYPES, part.source.mimeType, "document");
335
+ return isData ? {
336
+ type: "document",
337
+ data: part.source.value,
338
+ mime_type
339
+ } : {
340
+ type: "document",
341
+ uri: part.source.value,
342
+ mime_type
343
+ };
344
+ }
345
+ }
389
346
  }
390
347
  function convertToolsToInteractionsFormat(tools) {
391
- if (!tools || tools.length === 0) return void 0;
392
- const result = [];
393
- for (const tool of tools) {
394
- switch (tool.name) {
395
- case "google_search": {
396
- const metadata = tool.metadata ?? {};
397
- result.push({
398
- type: "google_search",
399
- ...metadata.search_types ? { search_types: metadata.search_types } : {}
400
- });
401
- break;
402
- }
403
- case "code_execution": {
404
- result.push({ type: "code_execution" });
405
- break;
406
- }
407
- case "url_context": {
408
- result.push({ type: "url_context" });
409
- break;
410
- }
411
- case "file_search": {
412
- const metadata = tool.metadata ?? {};
413
- result.push({
414
- type: "file_search",
415
- ...metadata.fileSearchStoreNames ? { file_search_store_names: metadata.fileSearchStoreNames } : {},
416
- ...metadata.topK !== void 0 ? { top_k: metadata.topK } : {},
417
- ...metadata.metadataFilter !== void 0 ? { metadata_filter: metadata.metadataFilter } : {}
418
- });
419
- break;
420
- }
421
- case "computer_use": {
422
- const metadata = tool.metadata ?? {};
423
- if (metadata.environment && metadata.environment !== "browser") {
424
- throw new Error(
425
- `computer_use environment "${metadata.environment}" is not supported on the Gemini Interactions API. Only "browser" is accepted.`
426
- );
427
- }
428
- result.push({
429
- type: "computer_use",
430
- ...metadata.environment ? { environment: metadata.environment } : {},
431
- ...metadata.excludedPredefinedFunctions ? {
432
- excludedPredefinedFunctions: metadata.excludedPredefinedFunctions
433
- } : {}
434
- });
435
- break;
436
- }
437
- case "google_search_retrieval":
438
- throw new Error(
439
- "`google_search_retrieval` is not supported on the Gemini Interactions API. Use `googleSearchTool()` (`google_search`) with `geminiTextInteractions()`, or call `geminiText()` for the legacy retrieval tool."
440
- );
441
- case "google_maps":
442
- throw new Error(
443
- "`google_maps` is not yet supported on the Gemini Interactions API. Use `geminiText()` for Google Maps grounding."
444
- );
445
- case "mcp_server":
446
- throw new Error(
447
- "`mcp_server` is not yet supported on the `geminiTextInteractions()` adapter."
448
- );
449
- default: {
450
- if (!tool.description) {
451
- throw new Error(
452
- `Tool ${tool.name} requires a description for the Gemini Interactions adapter`
453
- );
454
- }
455
- result.push({
456
- type: "function",
457
- name: tool.name,
458
- description: tool.description,
459
- parameters: sanitizeToolParameters(
460
- tool.inputSchema ?? { type: "object", properties: {} }
461
- )
462
- });
463
- }
464
- }
465
- }
466
- return result;
348
+ if (!tools || tools.length === 0) return void 0;
349
+ const result = [];
350
+ for (const tool of tools) switch (tool.name) {
351
+ case "google_search": {
352
+ const metadata = tool.metadata ?? {};
353
+ result.push({
354
+ type: "google_search",
355
+ ...metadata.search_types ? { search_types: metadata.search_types } : {}
356
+ });
357
+ break;
358
+ }
359
+ case "code_execution":
360
+ result.push({ type: "code_execution" });
361
+ break;
362
+ case "url_context":
363
+ result.push({ type: "url_context" });
364
+ break;
365
+ case "file_search": {
366
+ const metadata = tool.metadata ?? {};
367
+ result.push({
368
+ type: "file_search",
369
+ ...metadata.fileSearchStoreNames ? { file_search_store_names: metadata.fileSearchStoreNames } : {},
370
+ ...metadata.topK !== void 0 ? { top_k: metadata.topK } : {},
371
+ ...metadata.metadataFilter !== void 0 ? { metadata_filter: metadata.metadataFilter } : {}
372
+ });
373
+ break;
374
+ }
375
+ case "computer_use": {
376
+ const metadata = tool.metadata ?? {};
377
+ if (metadata.environment && metadata.environment !== "browser") throw new Error(`computer_use environment "${metadata.environment}" is not supported on the Gemini Interactions API. Only "browser" is accepted.`);
378
+ result.push({
379
+ type: "computer_use",
380
+ ...metadata.environment ? { environment: metadata.environment } : {},
381
+ ...metadata.excludedPredefinedFunctions ? { excludedPredefinedFunctions: metadata.excludedPredefinedFunctions } : {}
382
+ });
383
+ break;
384
+ }
385
+ case "google_search_retrieval": throw new Error("`google_search_retrieval` is not supported on the Gemini Interactions API. Use `googleSearchTool()` (`google_search`) with `geminiTextInteractions()`, or call `geminiText()` for the legacy retrieval tool.");
386
+ case "google_maps": throw new Error("`google_maps` is not yet supported on the Gemini Interactions API. Use `geminiText()` for Google Maps grounding.");
387
+ case "mcp_server": throw new Error("`mcp_server` is not yet supported on the `geminiTextInteractions()` adapter.");
388
+ default:
389
+ if (!tool.description) throw new Error(`Tool ${tool.name} requires a description for the Gemini Interactions adapter`);
390
+ result.push({
391
+ type: "function",
392
+ name: tool.name,
393
+ description: tool.description,
394
+ parameters: sanitizeToolParameters(tool.inputSchema ?? {
395
+ type: "object",
396
+ properties: {}
397
+ })
398
+ });
399
+ }
400
+ return result;
467
401
  }
468
402
  function statusToFinishReason(status, sawFunctionCall) {
469
- if (status === "requires_action") return "tool_calls";
470
- if (status === "incomplete") return "length";
471
- if (sawFunctionCall) return "tool_calls";
472
- return "stop";
403
+ if (status === "requires_action") return "tool_calls";
404
+ if (status === "incomplete") return "length";
405
+ if (sawFunctionCall) return "tool_calls";
406
+ return "stop";
473
407
  }
474
408
  function statusIsError(status) {
475
- return status === "failed" || status === "cancelled";
409
+ return status === "failed" || status === "cancelled";
476
410
  }
477
411
  async function* translateInteractionEvents(stream, model, runId, threadId, parentRunId, timestamp, adapterName, logger) {
478
- const messageId = generateId(adapterName);
479
- let hasEmittedRunStarted = false;
480
- let hasEmittedTextMessageStart = false;
481
- let textAccumulated = "";
482
- let interactionId;
483
- let sawFunctionCall = false;
484
- const toolCalls = /* @__PURE__ */ new Map();
485
- let nextToolIndex = 0;
486
- let thinkingStepId = null;
487
- let thinkingAccumulated = "";
488
- let reasoningMessageId = null;
489
- let hasClosedReasoning = false;
490
- const indexToToolCallId = /* @__PURE__ */ new Map();
491
- const argStringByToolCallId = /* @__PURE__ */ new Map();
492
- const closeReasoningIfNeeded = function* () {
493
- if (reasoningMessageId && !hasClosedReasoning) {
494
- hasClosedReasoning = true;
495
- yield {
496
- type: EventType.REASONING_MESSAGE_END,
497
- messageId: reasoningMessageId,
498
- model,
499
- timestamp
500
- };
501
- yield {
502
- type: EventType.REASONING_END,
503
- messageId: reasoningMessageId,
504
- model,
505
- timestamp
506
- };
507
- thinkingStepId = null;
508
- reasoningMessageId = null;
509
- hasClosedReasoning = false;
510
- }
511
- };
512
- const closeOpenState = function* () {
513
- yield* closeReasoningIfNeeded();
514
- for (const [toolCallId, state] of toolCalls) {
515
- if (state.ended) continue;
516
- state.ended = true;
517
- yield {
518
- type: EventType.TOOL_CALL_END,
519
- toolCallId,
520
- toolName: state.name,
521
- model,
522
- timestamp,
523
- input: state.args
524
- };
525
- }
526
- if (hasEmittedTextMessageStart) {
527
- hasEmittedTextMessageStart = false;
528
- yield {
529
- type: EventType.TEXT_MESSAGE_END,
530
- messageId,
531
- model,
532
- timestamp
533
- };
534
- }
535
- };
536
- const emitRunStartedIfNeeded = function* () {
537
- if (!hasEmittedRunStarted) {
538
- hasEmittedRunStarted = true;
539
- yield {
540
- type: EventType.RUN_STARTED,
541
- runId,
542
- threadId,
543
- model,
544
- timestamp,
545
- parentRunId
546
- };
547
- }
548
- };
549
- for await (const event of stream) {
550
- logger.provider(`provider=gemini-text-interactions`, { event });
551
- switch (event.event_type) {
552
- case "interaction.created": {
553
- interactionId = event.interaction.id;
554
- yield* emitRunStartedIfNeeded();
555
- break;
556
- }
557
- case "step.start": {
558
- yield* emitRunStartedIfNeeded();
559
- const step = event.step;
560
- const index = event.index;
561
- switch (step.type) {
562
- case "function_call": {
563
- yield* closeReasoningIfNeeded();
564
- sawFunctionCall = true;
565
- const toolCallId = step.id;
566
- indexToToolCallId.set(index, toolCallId);
567
- const initialArgs = step.arguments;
568
- const state = {
569
- name: step.name,
570
- args: { ...initialArgs },
571
- index: nextToolIndex++,
572
- started: true,
573
- ended: false
574
- };
575
- toolCalls.set(toolCallId, state);
576
- argStringByToolCallId.set(
577
- toolCallId,
578
- Object.keys(initialArgs).length > 0 ? JSON.stringify(initialArgs) : ""
579
- );
580
- yield {
581
- type: EventType.TOOL_CALL_START,
582
- toolCallId,
583
- toolCallName: state.name,
584
- toolName: state.name,
585
- // Bind the tool call to the same assistant message id the
586
- // eventual TEXT_MESSAGE_START uses so the message id stays
587
- // stable when a function_call arrives before any text (#477).
588
- parentMessageId: messageId,
589
- model,
590
- timestamp,
591
- index: state.index
592
- };
593
- if (Object.keys(initialArgs).length > 0) {
594
- const argsJson = JSON.stringify(initialArgs);
595
- yield {
596
- type: EventType.TOOL_CALL_ARGS,
597
- toolCallId,
598
- model,
599
- timestamp,
600
- delta: argsJson,
601
- args: argsJson
602
- };
603
- }
604
- break;
605
- }
606
- case "thought": {
607
- if (thinkingStepId === null || reasoningMessageId === null) {
608
- thinkingStepId = generateId(adapterName);
609
- reasoningMessageId = generateId(adapterName);
610
- yield {
611
- type: EventType.REASONING_START,
612
- messageId: reasoningMessageId,
613
- model,
614
- timestamp
615
- };
616
- yield {
617
- type: EventType.REASONING_MESSAGE_START,
618
- messageId: reasoningMessageId,
619
- role: "reasoning",
620
- model,
621
- timestamp
622
- };
623
- yield {
624
- type: EventType.STEP_STARTED,
625
- stepName: thinkingStepId,
626
- stepId: thinkingStepId,
627
- model,
628
- timestamp,
629
- stepType: "thinking"
630
- };
631
- }
632
- for (const part of step.summary ?? []) {
633
- if (part.type !== "text" || !part.text) continue;
634
- thinkingAccumulated += part.text;
635
- yield {
636
- type: EventType.REASONING_MESSAGE_CONTENT,
637
- messageId: reasoningMessageId,
638
- delta: part.text,
639
- model,
640
- timestamp
641
- };
642
- yield {
643
- type: EventType.STEP_FINISHED,
644
- stepName: thinkingStepId,
645
- stepId: thinkingStepId,
646
- model,
647
- timestamp,
648
- delta: part.text,
649
- content: thinkingAccumulated
650
- };
651
- }
652
- break;
653
- }
654
- case "model_output": {
655
- yield* closeReasoningIfNeeded();
656
- for (const part of step.content ?? []) {
657
- if (part.type !== "text" || !part.text) continue;
658
- if (!hasEmittedTextMessageStart) {
659
- hasEmittedTextMessageStart = true;
660
- yield {
661
- type: EventType.TEXT_MESSAGE_START,
662
- messageId,
663
- model,
664
- timestamp,
665
- role: "assistant"
666
- };
667
- }
668
- textAccumulated += part.text;
669
- yield {
670
- type: EventType.TEXT_MESSAGE_CONTENT,
671
- messageId,
672
- model,
673
- timestamp,
674
- delta: part.text,
675
- content: textAccumulated
676
- };
677
- }
678
- break;
679
- }
680
- case "google_search_call": {
681
- yield* closeReasoningIfNeeded();
682
- yield {
683
- type: EventType.CUSTOM,
684
- name: "gemini.googleSearchCall",
685
- value: step,
686
- model,
687
- timestamp
688
- };
689
- break;
690
- }
691
- case "google_search_result": {
692
- yield* closeReasoningIfNeeded();
693
- yield {
694
- type: EventType.CUSTOM,
695
- name: "gemini.googleSearchResult",
696
- value: step,
697
- model,
698
- timestamp
699
- };
700
- break;
701
- }
702
- case "code_execution_call": {
703
- yield* closeReasoningIfNeeded();
704
- yield {
705
- type: EventType.CUSTOM,
706
- name: "gemini.codeExecutionCall",
707
- value: step,
708
- model,
709
- timestamp
710
- };
711
- break;
712
- }
713
- case "code_execution_result": {
714
- yield* closeReasoningIfNeeded();
715
- yield {
716
- type: EventType.CUSTOM,
717
- name: "gemini.codeExecutionResult",
718
- value: step,
719
- model,
720
- timestamp
721
- };
722
- break;
723
- }
724
- case "url_context_call": {
725
- yield* closeReasoningIfNeeded();
726
- yield {
727
- type: EventType.CUSTOM,
728
- name: "gemini.urlContextCall",
729
- value: step,
730
- model,
731
- timestamp
732
- };
733
- break;
734
- }
735
- case "url_context_result": {
736
- yield* closeReasoningIfNeeded();
737
- yield {
738
- type: EventType.CUSTOM,
739
- name: "gemini.urlContextResult",
740
- value: step,
741
- model,
742
- timestamp
743
- };
744
- break;
745
- }
746
- case "file_search_call": {
747
- yield* closeReasoningIfNeeded();
748
- yield {
749
- type: EventType.CUSTOM,
750
- name: "gemini.fileSearchCall",
751
- value: step,
752
- model,
753
- timestamp
754
- };
755
- break;
756
- }
757
- case "file_search_result": {
758
- yield* closeReasoningIfNeeded();
759
- yield {
760
- type: EventType.CUSTOM,
761
- name: "gemini.fileSearchResult",
762
- value: step,
763
- model,
764
- timestamp
765
- };
766
- break;
767
- }
768
- // Unhandled step types (user_input on GET timelines,
769
- // mcp_server_*, google_maps_*, function_result) fall through
770
- // to the observability default so SDK drift is visible.
771
- case "user_input":
772
- case "mcp_server_tool_call":
773
- case "mcp_server_tool_result":
774
- case "google_maps_call":
775
- case "google_maps_result":
776
- case "function_result":
777
- default:
778
- logger.provider(`gemini-text-interactions unhandled step.start`, {
779
- step
780
- });
781
- break;
782
- }
783
- break;
784
- }
785
- case "step.delta": {
786
- yield* emitRunStartedIfNeeded();
787
- const delta = event.delta;
788
- const index = event.index;
789
- switch (delta.type) {
790
- case "text": {
791
- yield* closeReasoningIfNeeded();
792
- if (!hasEmittedTextMessageStart) {
793
- hasEmittedTextMessageStart = true;
794
- yield {
795
- type: EventType.TEXT_MESSAGE_START,
796
- messageId,
797
- model,
798
- timestamp,
799
- role: "assistant"
800
- };
801
- }
802
- textAccumulated += delta.text;
803
- yield {
804
- type: EventType.TEXT_MESSAGE_CONTENT,
805
- messageId,
806
- model,
807
- timestamp,
808
- delta: delta.text,
809
- content: textAccumulated
810
- };
811
- break;
812
- }
813
- case "arguments_delta": {
814
- const toolCallId = indexToToolCallId.get(index);
815
- if (!toolCallId) {
816
- logger.provider(
817
- `gemini-text-interactions arguments_delta for unknown step index`,
818
- { index, delta }
819
- );
820
- break;
821
- }
822
- const state = toolCalls.get(toolCallId);
823
- if (!state) break;
824
- const fragment = delta.arguments ?? "";
825
- const buffer = (argStringByToolCallId.get(toolCallId) ?? "") + fragment;
826
- argStringByToolCallId.set(toolCallId, buffer);
827
- const parsed = parsePartialToolArguments(buffer);
828
- if (parsed) state.args = parsed;
829
- yield {
830
- type: EventType.TOOL_CALL_ARGS,
831
- toolCallId,
832
- model,
833
- timestamp,
834
- delta: fragment,
835
- args: buffer
836
- };
837
- break;
838
- }
839
- case "thought_summary": {
840
- const thoughtText = delta.content && "text" in delta.content ? delta.content.text : "";
841
- if (!thoughtText) break;
842
- if (thinkingStepId === null || reasoningMessageId === null) {
843
- thinkingStepId = generateId(adapterName);
844
- reasoningMessageId = generateId(adapterName);
845
- yield {
846
- type: EventType.REASONING_START,
847
- messageId: reasoningMessageId,
848
- model,
849
- timestamp
850
- };
851
- yield {
852
- type: EventType.REASONING_MESSAGE_START,
853
- messageId: reasoningMessageId,
854
- role: "reasoning",
855
- model,
856
- timestamp
857
- };
858
- yield {
859
- type: EventType.STEP_STARTED,
860
- stepName: thinkingStepId,
861
- stepId: thinkingStepId,
862
- model,
863
- timestamp,
864
- stepType: "thinking"
865
- };
866
- }
867
- thinkingAccumulated += thoughtText;
868
- yield {
869
- type: EventType.REASONING_MESSAGE_CONTENT,
870
- messageId: reasoningMessageId,
871
- delta: thoughtText,
872
- model,
873
- timestamp
874
- };
875
- yield {
876
- type: EventType.STEP_FINISHED,
877
- stepName: thinkingStepId,
878
- stepId: thinkingStepId,
879
- model,
880
- timestamp,
881
- delta: thoughtText,
882
- content: thinkingAccumulated
883
- };
884
- break;
885
- }
886
- // The remaining StepDelta variants (image/audio/video/document
887
- // for output modalities a text adapter shouldn't see, tool
888
- // call/result deltas which are surfaced via step.start in this
889
- // adapter, thought_signature, annotation deltas, mcp/google
890
- // maps variants) fall through to the observability default.
891
- case "image":
892
- case "audio":
893
- case "video":
894
- case "document":
895
- case "thought_signature":
896
- case "text_annotation_delta":
897
- case "code_execution_call":
898
- case "code_execution_result":
899
- case "url_context_call":
900
- case "url_context_result":
901
- case "google_search_call":
902
- case "google_search_result":
903
- case "file_search_call":
904
- case "file_search_result":
905
- case "mcp_server_tool_call":
906
- case "mcp_server_tool_result":
907
- case "google_maps_call":
908
- case "google_maps_result":
909
- case "function_result":
910
- default:
911
- logger.provider(
912
- `gemini-text-interactions unhandled step.delta type`,
913
- { delta }
914
- );
915
- break;
916
- }
917
- break;
918
- }
919
- case "step.stop": {
920
- const toolCallId = indexToToolCallId.get(event.index);
921
- if (toolCallId) {
922
- const state = toolCalls.get(toolCallId);
923
- if (state && !state.ended) {
924
- state.ended = true;
925
- yield {
926
- type: EventType.TOOL_CALL_END,
927
- toolCallId,
928
- toolName: state.name,
929
- model,
930
- timestamp,
931
- input: state.args
932
- };
933
- }
934
- indexToToolCallId.delete(event.index);
935
- }
936
- break;
937
- }
938
- case "interaction.status_update": {
939
- break;
940
- }
941
- case "interaction.completed": {
942
- if (event.interaction.id) {
943
- interactionId = event.interaction.id;
944
- }
945
- yield* closeOpenState();
946
- const status = event.interaction.status;
947
- if (statusIsError(status)) {
948
- const message = `Gemini Interactions ${status}: the interaction ended without a usable response.`;
949
- logger.errors(
950
- "gemini-text-interactions.translateInteractionEvents non-success status",
951
- {
952
- source: "gemini-text-interactions.chatStream",
953
- status,
954
- interactionId
955
- }
956
- );
957
- yield {
958
- type: EventType.RUN_ERROR,
959
- runId,
960
- model,
961
- timestamp,
962
- message,
963
- code: status,
964
- error: { message, code: status }
965
- };
966
- return;
967
- }
968
- const usage = event.interaction.usage;
969
- const finishReason = statusToFinishReason(status, sawFunctionCall);
970
- if (interactionId) {
971
- yield {
972
- type: EventType.CUSTOM,
973
- name: "gemini.interactionId",
974
- value: { interactionId },
975
- model,
976
- timestamp
977
- };
978
- }
979
- yield {
980
- type: EventType.RUN_FINISHED,
981
- runId,
982
- threadId,
983
- model,
984
- timestamp,
985
- finishReason,
986
- usage: usage ? {
987
- promptTokens: usage.total_input_tokens ?? 0,
988
- completionTokens: usage.total_output_tokens ?? 0,
989
- totalTokens: usage.total_tokens ?? 0
990
- } : void 0
991
- };
992
- return;
993
- }
994
- case "error": {
995
- yield* closeOpenState();
996
- const rawMessage = event.error?.message;
997
- const message = typeof rawMessage === "string" && rawMessage.length > 0 ? rawMessage : `Gemini Interactions error (no message): ${JSON.stringify(event.error ?? {})}`;
998
- const rawCode = event.error?.code;
999
- const code = typeof rawCode === "string" || typeof rawCode === "number" ? String(rawCode) : void 0;
1000
- yield {
1001
- type: EventType.RUN_ERROR,
1002
- runId,
1003
- model,
1004
- timestamp,
1005
- message,
1006
- code,
1007
- error: { message, code }
1008
- };
1009
- return;
1010
- }
1011
- default:
1012
- logger.provider(`gemini-text-interactions unhandled event_type`, {
1013
- event
1014
- });
1015
- break;
1016
- }
1017
- }
1018
- yield* closeOpenState();
412
+ const messageId = generateId(adapterName);
413
+ let hasEmittedRunStarted = false;
414
+ let hasEmittedTextMessageStart = false;
415
+ let textAccumulated = "";
416
+ let interactionId;
417
+ let sawFunctionCall = false;
418
+ const toolCalls = /* @__PURE__ */ new Map();
419
+ let nextToolIndex = 0;
420
+ let thinkingStepId = null;
421
+ let thinkingAccumulated = "";
422
+ let reasoningMessageId = null;
423
+ let hasClosedReasoning = false;
424
+ const indexToToolCallId = /* @__PURE__ */ new Map();
425
+ const argStringByToolCallId = /* @__PURE__ */ new Map();
426
+ const closeReasoningIfNeeded = function* () {
427
+ if (reasoningMessageId && !hasClosedReasoning) {
428
+ hasClosedReasoning = true;
429
+ yield {
430
+ type: EventType.REASONING_MESSAGE_END,
431
+ messageId: reasoningMessageId,
432
+ model,
433
+ timestamp
434
+ };
435
+ yield {
436
+ type: EventType.REASONING_END,
437
+ messageId: reasoningMessageId,
438
+ model,
439
+ timestamp
440
+ };
441
+ thinkingStepId = null;
442
+ reasoningMessageId = null;
443
+ hasClosedReasoning = false;
444
+ }
445
+ };
446
+ const closeOpenState = function* () {
447
+ yield* closeReasoningIfNeeded();
448
+ for (const [toolCallId, state] of toolCalls) {
449
+ if (state.ended) continue;
450
+ state.ended = true;
451
+ yield {
452
+ type: EventType.TOOL_CALL_END,
453
+ toolCallId,
454
+ toolName: state.name,
455
+ model,
456
+ timestamp,
457
+ input: state.args
458
+ };
459
+ }
460
+ if (hasEmittedTextMessageStart) {
461
+ hasEmittedTextMessageStart = false;
462
+ yield {
463
+ type: EventType.TEXT_MESSAGE_END,
464
+ messageId,
465
+ model,
466
+ timestamp
467
+ };
468
+ }
469
+ };
470
+ const emitRunStartedIfNeeded = function* () {
471
+ if (!hasEmittedRunStarted) {
472
+ hasEmittedRunStarted = true;
473
+ yield {
474
+ type: EventType.RUN_STARTED,
475
+ runId,
476
+ threadId,
477
+ model,
478
+ timestamp,
479
+ parentRunId
480
+ };
481
+ }
482
+ };
483
+ for await (const event of stream) {
484
+ logger.provider(`provider=gemini-text-interactions`, { event });
485
+ switch (event.event_type) {
486
+ case "interaction.created":
487
+ interactionId = event.interaction.id;
488
+ yield* emitRunStartedIfNeeded();
489
+ break;
490
+ case "step.start": {
491
+ yield* emitRunStartedIfNeeded();
492
+ const step = event.step;
493
+ const index = event.index;
494
+ switch (step.type) {
495
+ case "function_call": {
496
+ yield* closeReasoningIfNeeded();
497
+ sawFunctionCall = true;
498
+ const toolCallId = step.id;
499
+ indexToToolCallId.set(index, toolCallId);
500
+ const initialArgs = step.arguments;
501
+ const state = {
502
+ name: step.name,
503
+ args: { ...initialArgs },
504
+ index: nextToolIndex++,
505
+ started: true,
506
+ ended: false
507
+ };
508
+ toolCalls.set(toolCallId, state);
509
+ argStringByToolCallId.set(toolCallId, Object.keys(initialArgs).length > 0 ? JSON.stringify(initialArgs) : "");
510
+ yield {
511
+ type: EventType.TOOL_CALL_START,
512
+ toolCallId,
513
+ toolCallName: state.name,
514
+ toolName: state.name,
515
+ parentMessageId: messageId,
516
+ model,
517
+ timestamp,
518
+ index: state.index
519
+ };
520
+ if (Object.keys(initialArgs).length > 0) {
521
+ const argsJson = JSON.stringify(initialArgs);
522
+ yield {
523
+ type: EventType.TOOL_CALL_ARGS,
524
+ toolCallId,
525
+ model,
526
+ timestamp,
527
+ delta: argsJson,
528
+ args: argsJson
529
+ };
530
+ }
531
+ break;
532
+ }
533
+ case "thought":
534
+ if (thinkingStepId === null || reasoningMessageId === null) {
535
+ thinkingStepId = generateId(adapterName);
536
+ reasoningMessageId = generateId(adapterName);
537
+ yield {
538
+ type: EventType.REASONING_START,
539
+ messageId: reasoningMessageId,
540
+ model,
541
+ timestamp
542
+ };
543
+ yield {
544
+ type: EventType.REASONING_MESSAGE_START,
545
+ messageId: reasoningMessageId,
546
+ role: "reasoning",
547
+ model,
548
+ timestamp
549
+ };
550
+ yield {
551
+ type: EventType.STEP_STARTED,
552
+ stepName: thinkingStepId,
553
+ stepId: thinkingStepId,
554
+ model,
555
+ timestamp,
556
+ stepType: "thinking"
557
+ };
558
+ }
559
+ for (const part of step.summary ?? []) {
560
+ if (part.type !== "text" || !part.text) continue;
561
+ thinkingAccumulated += part.text;
562
+ yield {
563
+ type: EventType.REASONING_MESSAGE_CONTENT,
564
+ messageId: reasoningMessageId,
565
+ delta: part.text,
566
+ model,
567
+ timestamp
568
+ };
569
+ yield {
570
+ type: EventType.STEP_FINISHED,
571
+ stepName: thinkingStepId,
572
+ stepId: thinkingStepId,
573
+ model,
574
+ timestamp,
575
+ delta: part.text,
576
+ content: thinkingAccumulated
577
+ };
578
+ }
579
+ break;
580
+ case "model_output":
581
+ yield* closeReasoningIfNeeded();
582
+ for (const part of step.content ?? []) {
583
+ if (part.type !== "text" || !part.text) continue;
584
+ if (!hasEmittedTextMessageStart) {
585
+ hasEmittedTextMessageStart = true;
586
+ yield {
587
+ type: EventType.TEXT_MESSAGE_START,
588
+ messageId,
589
+ model,
590
+ timestamp,
591
+ role: "assistant"
592
+ };
593
+ }
594
+ textAccumulated += part.text;
595
+ yield {
596
+ type: EventType.TEXT_MESSAGE_CONTENT,
597
+ messageId,
598
+ model,
599
+ timestamp,
600
+ delta: part.text,
601
+ content: textAccumulated
602
+ };
603
+ }
604
+ break;
605
+ case "google_search_call":
606
+ yield* closeReasoningIfNeeded();
607
+ yield {
608
+ type: EventType.CUSTOM,
609
+ name: "gemini.googleSearchCall",
610
+ value: step,
611
+ model,
612
+ timestamp
613
+ };
614
+ break;
615
+ case "google_search_result":
616
+ yield* closeReasoningIfNeeded();
617
+ yield {
618
+ type: EventType.CUSTOM,
619
+ name: "gemini.googleSearchResult",
620
+ value: step,
621
+ model,
622
+ timestamp
623
+ };
624
+ break;
625
+ case "code_execution_call":
626
+ yield* closeReasoningIfNeeded();
627
+ yield {
628
+ type: EventType.CUSTOM,
629
+ name: "gemini.codeExecutionCall",
630
+ value: step,
631
+ model,
632
+ timestamp
633
+ };
634
+ break;
635
+ case "code_execution_result":
636
+ yield* closeReasoningIfNeeded();
637
+ yield {
638
+ type: EventType.CUSTOM,
639
+ name: "gemini.codeExecutionResult",
640
+ value: step,
641
+ model,
642
+ timestamp
643
+ };
644
+ break;
645
+ case "url_context_call":
646
+ yield* closeReasoningIfNeeded();
647
+ yield {
648
+ type: EventType.CUSTOM,
649
+ name: "gemini.urlContextCall",
650
+ value: step,
651
+ model,
652
+ timestamp
653
+ };
654
+ break;
655
+ case "url_context_result":
656
+ yield* closeReasoningIfNeeded();
657
+ yield {
658
+ type: EventType.CUSTOM,
659
+ name: "gemini.urlContextResult",
660
+ value: step,
661
+ model,
662
+ timestamp
663
+ };
664
+ break;
665
+ case "file_search_call":
666
+ yield* closeReasoningIfNeeded();
667
+ yield {
668
+ type: EventType.CUSTOM,
669
+ name: "gemini.fileSearchCall",
670
+ value: step,
671
+ model,
672
+ timestamp
673
+ };
674
+ break;
675
+ case "file_search_result":
676
+ yield* closeReasoningIfNeeded();
677
+ yield {
678
+ type: EventType.CUSTOM,
679
+ name: "gemini.fileSearchResult",
680
+ value: step,
681
+ model,
682
+ timestamp
683
+ };
684
+ break;
685
+ default:
686
+ logger.provider(`gemini-text-interactions unhandled step.start`, { step });
687
+ break;
688
+ }
689
+ break;
690
+ }
691
+ case "step.delta": {
692
+ yield* emitRunStartedIfNeeded();
693
+ const delta = event.delta;
694
+ const index = event.index;
695
+ switch (delta.type) {
696
+ case "text":
697
+ yield* closeReasoningIfNeeded();
698
+ if (!hasEmittedTextMessageStart) {
699
+ hasEmittedTextMessageStart = true;
700
+ yield {
701
+ type: EventType.TEXT_MESSAGE_START,
702
+ messageId,
703
+ model,
704
+ timestamp,
705
+ role: "assistant"
706
+ };
707
+ }
708
+ textAccumulated += delta.text;
709
+ yield {
710
+ type: EventType.TEXT_MESSAGE_CONTENT,
711
+ messageId,
712
+ model,
713
+ timestamp,
714
+ delta: delta.text,
715
+ content: textAccumulated
716
+ };
717
+ break;
718
+ case "arguments_delta": {
719
+ const toolCallId = indexToToolCallId.get(index);
720
+ if (!toolCallId) {
721
+ logger.provider(`gemini-text-interactions arguments_delta for unknown step index`, {
722
+ index,
723
+ delta
724
+ });
725
+ break;
726
+ }
727
+ const state = toolCalls.get(toolCallId);
728
+ if (!state) break;
729
+ const fragment = delta.arguments ?? "";
730
+ const buffer = (argStringByToolCallId.get(toolCallId) ?? "") + fragment;
731
+ argStringByToolCallId.set(toolCallId, buffer);
732
+ const parsed = parsePartialToolArguments(buffer);
733
+ if (parsed) state.args = parsed;
734
+ yield {
735
+ type: EventType.TOOL_CALL_ARGS,
736
+ toolCallId,
737
+ model,
738
+ timestamp,
739
+ delta: fragment,
740
+ args: buffer
741
+ };
742
+ break;
743
+ }
744
+ case "thought_summary": {
745
+ const thoughtText = delta.content && "text" in delta.content ? delta.content.text : "";
746
+ if (!thoughtText) break;
747
+ if (thinkingStepId === null || reasoningMessageId === null) {
748
+ thinkingStepId = generateId(adapterName);
749
+ reasoningMessageId = generateId(adapterName);
750
+ yield {
751
+ type: EventType.REASONING_START,
752
+ messageId: reasoningMessageId,
753
+ model,
754
+ timestamp
755
+ };
756
+ yield {
757
+ type: EventType.REASONING_MESSAGE_START,
758
+ messageId: reasoningMessageId,
759
+ role: "reasoning",
760
+ model,
761
+ timestamp
762
+ };
763
+ yield {
764
+ type: EventType.STEP_STARTED,
765
+ stepName: thinkingStepId,
766
+ stepId: thinkingStepId,
767
+ model,
768
+ timestamp,
769
+ stepType: "thinking"
770
+ };
771
+ }
772
+ thinkingAccumulated += thoughtText;
773
+ yield {
774
+ type: EventType.REASONING_MESSAGE_CONTENT,
775
+ messageId: reasoningMessageId,
776
+ delta: thoughtText,
777
+ model,
778
+ timestamp
779
+ };
780
+ yield {
781
+ type: EventType.STEP_FINISHED,
782
+ stepName: thinkingStepId,
783
+ stepId: thinkingStepId,
784
+ model,
785
+ timestamp,
786
+ delta: thoughtText,
787
+ content: thinkingAccumulated
788
+ };
789
+ break;
790
+ }
791
+ default:
792
+ logger.provider(`gemini-text-interactions unhandled step.delta type`, { delta });
793
+ break;
794
+ }
795
+ break;
796
+ }
797
+ case "step.stop": {
798
+ const toolCallId = indexToToolCallId.get(event.index);
799
+ if (toolCallId) {
800
+ const state = toolCalls.get(toolCallId);
801
+ if (state && !state.ended) {
802
+ state.ended = true;
803
+ yield {
804
+ type: EventType.TOOL_CALL_END,
805
+ toolCallId,
806
+ toolName: state.name,
807
+ model,
808
+ timestamp,
809
+ input: state.args
810
+ };
811
+ }
812
+ indexToToolCallId.delete(event.index);
813
+ }
814
+ break;
815
+ }
816
+ case "interaction.status_update": break;
817
+ case "interaction.completed": {
818
+ if (event.interaction.id) interactionId = event.interaction.id;
819
+ yield* closeOpenState();
820
+ const status = event.interaction.status;
821
+ if (statusIsError(status)) {
822
+ const message = `Gemini Interactions ${status}: the interaction ended without a usable response.`;
823
+ logger.errors("gemini-text-interactions.translateInteractionEvents non-success status", {
824
+ source: "gemini-text-interactions.chatStream",
825
+ status,
826
+ interactionId
827
+ });
828
+ yield {
829
+ type: EventType.RUN_ERROR,
830
+ runId,
831
+ model,
832
+ timestamp,
833
+ message,
834
+ code: status,
835
+ error: {
836
+ message,
837
+ code: status
838
+ }
839
+ };
840
+ return;
841
+ }
842
+ const usage = event.interaction.usage;
843
+ const finishReason = statusToFinishReason(status, sawFunctionCall);
844
+ if (interactionId) yield {
845
+ type: EventType.CUSTOM,
846
+ name: "gemini.interactionId",
847
+ value: { interactionId },
848
+ model,
849
+ timestamp
850
+ };
851
+ yield {
852
+ type: EventType.RUN_FINISHED,
853
+ runId,
854
+ threadId,
855
+ model,
856
+ timestamp,
857
+ finishReason,
858
+ usage: usage ? {
859
+ promptTokens: usage.total_input_tokens ?? 0,
860
+ completionTokens: usage.total_output_tokens ?? 0,
861
+ totalTokens: usage.total_tokens ?? 0
862
+ } : void 0
863
+ };
864
+ return;
865
+ }
866
+ case "error": {
867
+ yield* closeOpenState();
868
+ const rawMessage = event.error?.message;
869
+ const message = typeof rawMessage === "string" && rawMessage.length > 0 ? rawMessage : `Gemini Interactions error (no message): ${JSON.stringify(event.error ?? {})}`;
870
+ const rawCode = event.error?.code;
871
+ const code = typeof rawCode === "string" || typeof rawCode === "number" ? String(rawCode) : void 0;
872
+ yield {
873
+ type: EventType.RUN_ERROR,
874
+ runId,
875
+ model,
876
+ timestamp,
877
+ message,
878
+ code,
879
+ error: {
880
+ message,
881
+ code
882
+ }
883
+ };
884
+ return;
885
+ }
886
+ default:
887
+ logger.provider(`gemini-text-interactions unhandled event_type`, { event });
888
+ break;
889
+ }
890
+ }
891
+ yield* closeOpenState();
1019
892
  }
1020
893
  function extractTextFromInteraction(interaction) {
1021
- if (typeof interaction.output_text === "string" && interaction.output_text) {
1022
- return interaction.output_text;
1023
- }
1024
- let text = "";
1025
- for (const step of interaction.steps ?? []) {
1026
- if (step.type !== "model_output" || !step.content) continue;
1027
- for (const part of step.content) {
1028
- if (part.type === "text") {
1029
- text += part.text;
1030
- }
1031
- }
1032
- }
1033
- return text;
894
+ if (typeof interaction.output_text === "string" && interaction.output_text) return interaction.output_text;
895
+ let text = "";
896
+ for (const step of interaction.steps ?? []) {
897
+ if (step.type !== "model_output" || !step.content) continue;
898
+ for (const part of step.content) if (part.type === "text") text += part.text;
899
+ }
900
+ return text;
1034
901
  }
1035
902
  function sanitizeToolParameters(schema) {
1036
- if (!schema || typeof schema !== "object") return schema;
1037
- if (Array.isArray(schema)) return schema.map(sanitizeToolParameters);
1038
- const out = {};
1039
- for (const [key, value] of Object.entries(schema)) {
1040
- if (key === "required" && Array.isArray(value) && value.length === 0) {
1041
- continue;
1042
- }
1043
- out[key] = sanitizeToolParameters(value);
1044
- }
1045
- return out;
903
+ if (!schema || typeof schema !== "object") return schema;
904
+ if (Array.isArray(schema)) return schema.map(sanitizeToolParameters);
905
+ const out = {};
906
+ for (const [key, value] of Object.entries(schema)) {
907
+ if (key === "required" && Array.isArray(value) && value.length === 0) continue;
908
+ out[key] = sanitizeToolParameters(value);
909
+ }
910
+ return out;
1046
911
  }
1047
- export {
1048
- GeminiTextInteractionsAdapter,
1049
- createGeminiTextInteractions,
1050
- geminiTextInteractions
1051
- };
1052
- //# sourceMappingURL=adapter.js.map
912
+ //#endregion
913
+ export { GeminiTextInteractionsAdapter, createGeminiTextInteractions, geminiTextInteractions };
914
+
915
+ //# sourceMappingURL=adapter.js.map