@oh-my-pi/pi-ai 16.2.5 → 16.2.7

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 (31) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/types/auth-storage.d.ts +11 -10
  3. package/dist/types/providers/__tests__/kimi-code-thinking.test.d.ts +1 -0
  4. package/dist/types/providers/google-auth.d.ts +8 -0
  5. package/dist/types/providers/google-interactions.d.ts +65 -0
  6. package/dist/types/providers/google-shared.d.ts +20 -5
  7. package/dist/types/providers/google-types.d.ts +5 -0
  8. package/dist/types/providers/openai-codex/request-transformer.d.ts +1 -1
  9. package/dist/types/providers/openai-codex-responses.d.ts +1 -1
  10. package/dist/types/providers/openai-shared.d.ts +3 -3
  11. package/dist/types/types.d.ts +80 -30
  12. package/dist/types/utils/leaked-thinking-stream.d.ts +29 -0
  13. package/package.json +4 -4
  14. package/src/auth-storage.ts +56 -52
  15. package/src/providers/__tests__/kimi-code-thinking.test.ts +112 -0
  16. package/src/providers/anthropic.ts +11 -10
  17. package/src/providers/google-auth.ts +25 -0
  18. package/src/providers/google-gemini-cli.ts +90 -41
  19. package/src/providers/google-interactions.ts +753 -0
  20. package/src/providers/google-shared.ts +74 -13
  21. package/src/providers/google-types.ts +5 -1
  22. package/src/providers/google-vertex.ts +117 -26
  23. package/src/providers/google.ts +61 -19
  24. package/src/providers/openai-chat-server.ts +2 -2
  25. package/src/providers/openai-codex/request-transformer.ts +17 -4
  26. package/src/providers/openai-codex-responses.ts +23 -5
  27. package/src/providers/openai-shared.ts +5 -11
  28. package/src/stream.ts +34 -12
  29. package/src/types.ts +164 -51
  30. package/src/usage/google-antigravity.ts +59 -5
  31. package/src/utils/leaked-thinking-stream.ts +260 -0
@@ -0,0 +1,753 @@
1
+ import { parseGeminiModel } from "@oh-my-pi/pi-catalog/identity";
2
+ import { calculateCost } from "@oh-my-pi/pi-catalog/models";
3
+ import { fetchWithRetry, readSseJson } from "@oh-my-pi/pi-utils";
4
+ import * as AIError from "../error";
5
+ import type {
6
+ AssistantMessage,
7
+ Context,
8
+ FetchImpl,
9
+ ImageContent,
10
+ Message,
11
+ Model,
12
+ ProviderSessionState,
13
+ TextContent,
14
+ ToolCall,
15
+ Usage,
16
+ } from "../types";
17
+ import { shouldSendServiceTier } from "../types";
18
+ import { normalizeSystemPrompts } from "../utils";
19
+ import { AssistantMessageEventStream } from "../utils/event-stream";
20
+ import { convertTools, type GoogleSharedStreamOptions, type GoogleThinkingLevel } from "./google-shared";
21
+
22
+ type GoogleInteractionsApi = "google-generative-ai" | "google-vertex";
23
+ type GoogleInteractionsModel = Model<GoogleInteractionsApi>;
24
+ type GoogleOptions = GoogleSharedStreamOptions;
25
+
26
+ const GOOGLE_INTERACTIONS_STATE_KEY = "google-interactions-state";
27
+
28
+ /** Provider session state storing the last Gemini Interactions response id. */
29
+ export interface GoogleInteractionsProviderSessionState extends ProviderSessionState {
30
+ lastInteractionId?: string;
31
+ }
32
+
33
+ /** Conversation anchor for continuing an Interactions turn from a prior assistant response. */
34
+ export interface InteractionAnchor {
35
+ id?: string;
36
+ messageIndex?: number;
37
+ }
38
+
39
+ type InteractionContent = { type: "text"; text: string } | { type: "image"; data: string; mime_type: string };
40
+
41
+ interface InteractionUserInputStep {
42
+ type: "user_input";
43
+ content: InteractionContent[];
44
+ }
45
+
46
+ interface InteractionModelOutputStep {
47
+ type: "model_output";
48
+ content: InteractionContent[];
49
+ }
50
+
51
+ interface InteractionFunctionCallStep {
52
+ type: "function_call";
53
+ id: string;
54
+ name: string;
55
+ arguments: Record<string, unknown>;
56
+ }
57
+
58
+ interface InteractionFunctionResultStep {
59
+ type: "function_result";
60
+ name: string;
61
+ call_id: string;
62
+ result: InteractionContent[];
63
+ is_error?: boolean;
64
+ }
65
+
66
+ interface InteractionThoughtStep {
67
+ type: "thought";
68
+ summary?: InteractionContent[];
69
+ signature?: string;
70
+ }
71
+
72
+ interface InteractionThoughtSummaryDelta {
73
+ type: "thought_summary";
74
+ content?: InteractionContent;
75
+ }
76
+
77
+ interface InteractionThoughtSignatureDelta {
78
+ type: "thought_signature";
79
+ signature?: string;
80
+ }
81
+
82
+ interface InteractionArgumentsDelta {
83
+ type: "arguments_delta";
84
+ arguments?: string;
85
+ }
86
+
87
+ interface PendingInteractionToolCall {
88
+ id: string;
89
+ name: string;
90
+ argumentsText: string;
91
+ argumentsObject: Record<string, unknown>;
92
+ }
93
+
94
+ type InteractionInputStep =
95
+ | InteractionUserInputStep
96
+ | InteractionModelOutputStep
97
+ | InteractionFunctionCallStep
98
+ | InteractionFunctionResultStep;
99
+
100
+ type InteractionStep =
101
+ | InteractionModelOutputStep
102
+ | InteractionFunctionCallStep
103
+ | InteractionFunctionResultStep
104
+ | InteractionThoughtStep
105
+ | InteractionUserInputStep;
106
+
107
+ type InteractionThinkingLevel = "minimal" | "low" | "medium" | "high";
108
+
109
+ interface InteractionGenerationConfig {
110
+ temperature?: number;
111
+ top_p?: number;
112
+ top_k?: number;
113
+ min_p?: number;
114
+ presence_penalty?: number;
115
+ frequency_penalty?: number;
116
+ repetition_penalty?: number;
117
+ max_output_tokens?: number;
118
+ thinking_level?: InteractionThinkingLevel;
119
+ thinking_budget?: number;
120
+ }
121
+
122
+ interface GoogleInteractionRequest {
123
+ model: string;
124
+ input: InteractionInputStep[];
125
+ stream: true;
126
+ previous_interaction_id?: string;
127
+ system_instruction?: string;
128
+ tools?: { functionDeclarations: Record<string, unknown>[] }[];
129
+ store?: boolean;
130
+ generation_config?: InteractionGenerationConfig;
131
+ service_tier?: string;
132
+ }
133
+
134
+ interface InteractionUsage {
135
+ total_input_tokens?: number;
136
+ total_cached_tokens?: number;
137
+ total_output_tokens?: number;
138
+ total_thought_tokens?: number;
139
+ total_tokens?: number;
140
+ }
141
+
142
+ interface InteractionResource {
143
+ id?: string;
144
+ status?: string;
145
+ usage?: InteractionUsage;
146
+ }
147
+
148
+ interface InteractionStreamMetadata {
149
+ total_usage?: InteractionUsage;
150
+ }
151
+
152
+ interface InteractionSseEvent {
153
+ event_type?: string;
154
+ index?: number;
155
+ step?: InteractionStep;
156
+ delta?:
157
+ | InteractionContent
158
+ | InteractionFunctionCallStep
159
+ | InteractionThoughtStep
160
+ | InteractionThoughtSummaryDelta
161
+ | InteractionThoughtSignatureDelta
162
+ | InteractionArgumentsDelta;
163
+ interaction?: InteractionResource;
164
+ interaction_id?: string;
165
+ status?: string;
166
+ metadata?: InteractionStreamMetadata;
167
+ error?: { message?: string; code?: string | number };
168
+ }
169
+
170
+ function emptyUsage(): Usage {
171
+ return {
172
+ input: 0,
173
+ output: 0,
174
+ cacheRead: 0,
175
+ cacheWrite: 0,
176
+ totalTokens: 0,
177
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
178
+ };
179
+ }
180
+
181
+ function getGoogleInteractionsState(
182
+ providerSessionState: Map<string, ProviderSessionState> | undefined,
183
+ create: boolean,
184
+ ): GoogleInteractionsProviderSessionState | undefined {
185
+ if (!providerSessionState) return undefined;
186
+ const existing = providerSessionState.get(GOOGLE_INTERACTIONS_STATE_KEY) as
187
+ | GoogleInteractionsProviderSessionState
188
+ | undefined;
189
+ if (existing || !create) return existing;
190
+ const state: GoogleInteractionsProviderSessionState = { close: () => {} };
191
+ providerSessionState.set(GOOGLE_INTERACTIONS_STATE_KEY, state);
192
+ return state;
193
+ }
194
+
195
+ function interactionContentFromText(text: string): InteractionContent[] {
196
+ return text.length === 0 ? [] : [{ type: "text", text }];
197
+ }
198
+
199
+ function interactionContentFromParts(parts: readonly (TextContent | ImageContent)[]): InteractionContent[] {
200
+ const content: InteractionContent[] = [];
201
+ for (const part of parts) {
202
+ if (part.type === "text") {
203
+ if (part.text.length > 0) content.push({ type: "text", text: part.text });
204
+ } else {
205
+ content.push({ type: "image", data: part.data, mime_type: part.mimeType });
206
+ }
207
+ }
208
+ return content;
209
+ }
210
+
211
+ function userInputStepFromMessage(message: Extract<Message, { role: "user" | "developer" }>): InteractionUserInputStep {
212
+ const content =
213
+ typeof message.content === "string"
214
+ ? interactionContentFromText(message.content)
215
+ : interactionContentFromParts(message.content);
216
+ return { type: "user_input", content };
217
+ }
218
+
219
+ function functionResultStepFromMessage(
220
+ message: Extract<Message, { role: "toolResult" }>,
221
+ ): InteractionFunctionResultStep {
222
+ const result = interactionContentFromParts(message.content);
223
+ return {
224
+ type: "function_result",
225
+ name: message.toolName,
226
+ call_id: message.toolCallId,
227
+ result: result.length > 0 ? result : [{ type: "text", text: "" }],
228
+ ...(message.isError ? { is_error: true } : {}),
229
+ };
230
+ }
231
+
232
+ function appendAssistantInteractionSteps(message: AssistantMessage, steps: InteractionInputStep[]): void {
233
+ let modelContent: InteractionContent[] = [];
234
+ const flushModelContent = (): void => {
235
+ if (modelContent.length === 0) return;
236
+ steps.push({ type: "model_output", content: modelContent });
237
+ modelContent = [];
238
+ };
239
+
240
+ for (const block of message.content) {
241
+ if (block.type === "text") {
242
+ if (block.text.length > 0) modelContent.push({ type: "text", text: block.text });
243
+ } else if (block.type === "toolCall") {
244
+ flushModelContent();
245
+ steps.push({ type: "function_call", id: block.id, name: block.name, arguments: block.arguments });
246
+ }
247
+ }
248
+ flushModelContent();
249
+ }
250
+
251
+ function interactionMessagesAfterAnchor(
252
+ messages: readonly Message[],
253
+ anchorIndex: number | undefined,
254
+ ): readonly Message[] {
255
+ return anchorIndex === undefined ? messages : messages.slice(anchorIndex + 1);
256
+ }
257
+
258
+ function buildInteractionInput(context: Context, anchorIndex: number | undefined): InteractionInputStep[] {
259
+ const input: InteractionInputStep[] = [];
260
+ for (const message of interactionMessagesAfterAnchor(context.messages, anchorIndex)) {
261
+ if (message.role === "user" || message.role === "developer") {
262
+ const step = userInputStepFromMessage(message);
263
+ if (step.content.length > 0) input.push(step);
264
+ } else if (message.role === "toolResult") {
265
+ input.push(functionResultStepFromMessage(message));
266
+ } else if (anchorIndex === undefined) {
267
+ appendAssistantInteractionSteps(message, input);
268
+ }
269
+ }
270
+ return input.length > 0 ? input : [{ type: "user_input", content: [{ type: "text", text: "" }] }];
271
+ }
272
+
273
+ function toInteractionThinkingLevel(level: GoogleThinkingLevel): InteractionThinkingLevel | undefined {
274
+ switch (level) {
275
+ case "MINIMAL":
276
+ return "minimal";
277
+ case "LOW":
278
+ return "low";
279
+ case "MEDIUM":
280
+ return "medium";
281
+ case "HIGH":
282
+ return "high";
283
+ case "THINKING_LEVEL_UNSPECIFIED":
284
+ return undefined;
285
+ }
286
+ }
287
+
288
+ function buildInteractionGenerationConfig(options: GoogleOptions | undefined): InteractionGenerationConfig | undefined {
289
+ const config: InteractionGenerationConfig = {};
290
+ if (options?.temperature !== undefined) config.temperature = options.temperature;
291
+ if (options?.topP !== undefined) config.top_p = options.topP;
292
+ if (options?.topK !== undefined) config.top_k = options.topK;
293
+ if (options?.minP !== undefined) config.min_p = options.minP;
294
+ if (options?.presencePenalty !== undefined) config.presence_penalty = options.presencePenalty;
295
+ if (options?.frequencyPenalty !== undefined) config.frequency_penalty = options.frequencyPenalty;
296
+ if (options?.repetitionPenalty !== undefined) config.repetition_penalty = options.repetitionPenalty;
297
+ if (options?.maxTokens !== undefined) config.max_output_tokens = options.maxTokens;
298
+ if (options?.thinking?.level !== undefined) {
299
+ const thinkingLevel = toInteractionThinkingLevel(options.thinking.level);
300
+ if (thinkingLevel !== undefined) config.thinking_level = thinkingLevel;
301
+ } else if (options?.thinking?.budgetTokens !== undefined) {
302
+ config.thinking_budget = options.thinking.budgetTokens;
303
+ }
304
+ return Object.keys(config).length > 0 ? config : undefined;
305
+ }
306
+
307
+ function buildInteractionRequest(
308
+ model: GoogleInteractionsModel,
309
+ context: Context,
310
+ options: GoogleOptions | undefined,
311
+ anchor: InteractionAnchor,
312
+ ): GoogleInteractionRequest {
313
+ const systemInstruction = normalizeSystemPrompts(context.systemPrompt).join("\n\n");
314
+ const generationConfig = buildInteractionGenerationConfig(options);
315
+ return {
316
+ model: model.id,
317
+ input: buildInteractionInput(context, anchor.messageIndex),
318
+ stream: true,
319
+ ...(anchor.id !== undefined ? { previous_interaction_id: anchor.id } : {}),
320
+ ...(systemInstruction.length > 0 ? { system_instruction: systemInstruction } : {}),
321
+ ...(context.tools && context.tools.length > 0 ? { tools: convertTools(context.tools, model) } : {}),
322
+ ...(options?.storeInteraction !== undefined ? { store: options.storeInteraction } : {}),
323
+ ...(generationConfig !== undefined ? { generation_config: generationConfig } : {}),
324
+ ...(shouldSendServiceTier(options?.serviceTier, model.provider) ? { service_tier: options?.serviceTier } : {}),
325
+ };
326
+ }
327
+
328
+ function applyInteractionUsage(
329
+ model: GoogleInteractionsModel,
330
+ output: AssistantMessage,
331
+ usage: InteractionUsage,
332
+ ): void {
333
+ const thinkingTokens = usage.total_thought_tokens ?? 0;
334
+ output.usage = {
335
+ input: (usage.total_input_tokens ?? 0) - (usage.total_cached_tokens ?? 0),
336
+ output: (usage.total_output_tokens ?? 0) + thinkingTokens,
337
+ cacheRead: usage.total_cached_tokens ?? 0,
338
+ cacheWrite: 0,
339
+ totalTokens: usage.total_tokens ?? 0,
340
+ ...(thinkingTokens > 0 ? { reasoningTokens: thinkingTokens } : {}),
341
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
342
+ };
343
+ calculateCost(model, output.usage);
344
+ }
345
+
346
+ function parseInteractionFunctionCall(value: unknown): InteractionFunctionCallStep | undefined {
347
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
348
+ const record = value as Record<string, unknown>;
349
+ if (record.type !== "function_call") return undefined;
350
+ if (typeof record.id !== "string" || typeof record.name !== "string") return undefined;
351
+ const args = record.arguments;
352
+ return {
353
+ type: "function_call",
354
+ id: record.id,
355
+ name: record.name,
356
+ arguments: args && typeof args === "object" && !Array.isArray(args) ? { ...args } : {},
357
+ };
358
+ }
359
+
360
+ function pendingToolCallFromStep(call: InteractionFunctionCallStep): PendingInteractionToolCall {
361
+ return {
362
+ id: call.id,
363
+ name: call.name,
364
+ argumentsText: "",
365
+ argumentsObject: call.arguments,
366
+ };
367
+ }
368
+
369
+ function parseInteractionArguments(text: string): Record<string, unknown> {
370
+ if (text.trim().length === 0) return {};
371
+ try {
372
+ const parsed: unknown = JSON.parse(text);
373
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return { ...parsed };
374
+ } catch {
375
+ return {};
376
+ }
377
+ return {};
378
+ }
379
+
380
+ /** Provider-specific URL, headers, and fetch implementation for an Interactions request. */
381
+ export interface GoogleInteractionsPlan {
382
+ url: string;
383
+ headers: Record<string, string>;
384
+ fetch?: FetchImpl;
385
+ }
386
+
387
+ /**
388
+ * Streams Gemini Interactions API model-mode responses for direct Google and Vertex providers.
389
+ *
390
+ * `fallback`, when supplied, is the legacy `:streamGenerateContent` stream factory. It runs
391
+ * transparently — forwarding its events into this stream — when the Interactions attempt fails
392
+ * before any content is emitted with a signal that the model/endpoint does not support
393
+ * Interactions (HTTP 404/400). Provide it only for auto-selected Interactions requests so an
394
+ * explicit `useInteractionsApi: true` still surfaces failures.
395
+ */
396
+ export function streamGoogleInteractions<T extends GoogleInteractionsApi>(args: {
397
+ model: Model<T>;
398
+ context: Context;
399
+ options: GoogleSharedStreamOptions | undefined;
400
+ api: T;
401
+ anchor: InteractionAnchor;
402
+ state: GoogleInteractionsProviderSessionState | undefined;
403
+ prepare: () => GoogleInteractionsPlan | Promise<GoogleInteractionsPlan>;
404
+ fallback?: () => AssistantMessageEventStream;
405
+ }): AssistantMessageEventStream {
406
+ const { model, context, options, anchor, state } = args;
407
+ const stream = new AssistantMessageEventStream();
408
+ const output: AssistantMessage = {
409
+ role: "assistant",
410
+ content: [],
411
+ api: args.api,
412
+ provider: model.provider,
413
+ model: model.id,
414
+ usage: emptyUsage(),
415
+ stopReason: "stop",
416
+ timestamp: Date.now(),
417
+ };
418
+ const storeInteraction = options?.storeInteraction !== false;
419
+
420
+ void (async () => {
421
+ let started = false;
422
+ let sawTerminal = false;
423
+ let currentTextBlock: TextContent | undefined;
424
+ let currentThinkingBlock: Extract<AssistantMessage["content"][number], { type: "thinking" }> | undefined;
425
+ let pendingThinkingSignature: string | undefined;
426
+ const stepKinds = new Map<number, string>();
427
+ const pendingToolCalls = new Map<number, PendingInteractionToolCall>();
428
+ const ensureStarted = (): void => {
429
+ if (started) return;
430
+ stream.push({ type: "start", partial: output });
431
+ started = true;
432
+ };
433
+ const endOpenBlocks = (): void => {
434
+ if (currentTextBlock) {
435
+ stream.push({
436
+ type: "text_end",
437
+ contentIndex: output.content.indexOf(currentTextBlock),
438
+ content: currentTextBlock.text,
439
+ partial: output,
440
+ });
441
+ currentTextBlock = undefined;
442
+ }
443
+ if (currentThinkingBlock) {
444
+ stream.push({
445
+ type: "thinking_end",
446
+ contentIndex: output.content.indexOf(currentThinkingBlock),
447
+ content: currentThinkingBlock.thinking,
448
+ partial: output,
449
+ });
450
+ currentThinkingBlock = undefined;
451
+ }
452
+ };
453
+ const emitText = (text: string): void => {
454
+ if (text.length === 0) return;
455
+ ensureStarted();
456
+ if (!currentTextBlock) {
457
+ if (currentThinkingBlock) endOpenBlocks();
458
+ currentTextBlock = { type: "text", text: "" };
459
+ output.content.push(currentTextBlock);
460
+ stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output });
461
+ }
462
+ currentTextBlock.text += text;
463
+ stream.push({
464
+ type: "text_delta",
465
+ contentIndex: output.content.indexOf(currentTextBlock),
466
+ delta: text,
467
+ partial: output,
468
+ });
469
+ };
470
+ const applyThinkingSignature = (signature: string | undefined): void => {
471
+ if (!signature) return;
472
+ if (currentThinkingBlock) {
473
+ currentThinkingBlock.thinkingSignature = signature;
474
+ } else {
475
+ pendingThinkingSignature = signature;
476
+ }
477
+ };
478
+ const emitThinking = (text: string): void => {
479
+ if (text.length === 0) return;
480
+ ensureStarted();
481
+ if (!currentThinkingBlock) {
482
+ if (currentTextBlock) endOpenBlocks();
483
+ currentThinkingBlock = { type: "thinking", thinking: "", thinkingSignature: pendingThinkingSignature };
484
+ pendingThinkingSignature = undefined;
485
+ output.content.push(currentThinkingBlock);
486
+ stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
487
+ }
488
+ currentThinkingBlock.thinking += text;
489
+ stream.push({
490
+ type: "thinking_delta",
491
+ contentIndex: output.content.indexOf(currentThinkingBlock),
492
+ delta: text,
493
+ partial: output,
494
+ });
495
+ };
496
+ const emitToolCall = (call: InteractionFunctionCallStep): void => {
497
+ ensureStarted();
498
+ endOpenBlocks();
499
+ const toolCall: ToolCall = {
500
+ type: "toolCall",
501
+ id: call.id,
502
+ name: call.name,
503
+ arguments: call.arguments,
504
+ };
505
+ output.content.push(toolCall);
506
+ const contentIndex = output.content.length - 1;
507
+ stream.push({ type: "toolcall_start", contentIndex, partial: output });
508
+ stream.push({
509
+ type: "toolcall_delta",
510
+ contentIndex,
511
+ delta: JSON.stringify(toolCall.arguments),
512
+ partial: output,
513
+ });
514
+ stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
515
+ };
516
+ const emitPendingToolCall = (pending: PendingInteractionToolCall): void => {
517
+ emitToolCall({
518
+ type: "function_call",
519
+ id: pending.id,
520
+ name: pending.name,
521
+ arguments:
522
+ pending.argumentsText.length > 0
523
+ ? parseInteractionArguments(pending.argumentsText)
524
+ : pending.argumentsObject,
525
+ });
526
+ };
527
+
528
+ let prepared = false;
529
+ try {
530
+ const plan = await args.prepare();
531
+ prepared = true;
532
+ let requestBody: unknown = buildInteractionRequest(model, context, options, anchor);
533
+ const replacement = await options?.onPayload?.(requestBody, model);
534
+ if (replacement !== undefined) requestBody = replacement;
535
+ const response = await fetchWithRetry(() => plan.url, {
536
+ method: "POST",
537
+ headers: {
538
+ ...plan.headers,
539
+ "Content-Type": "application/json",
540
+ Accept: "text/event-stream",
541
+ },
542
+ body: JSON.stringify(requestBody),
543
+ signal: options?.signal,
544
+ fetch: plan.fetch,
545
+ });
546
+ if (!response.ok) {
547
+ const errorText = await response.text().catch(() => "");
548
+ throw new AIError.GoogleApiError(
549
+ `Google Interactions API error (${response.status}): ${errorText}`,
550
+ response.status,
551
+ {
552
+ headers: response.headers,
553
+ },
554
+ );
555
+ }
556
+ if (!response.body) {
557
+ throw new AIError.ProviderResponseError("Google Interactions API returned an empty response body", {
558
+ provider: model.provider,
559
+ kind: "empty-body",
560
+ });
561
+ }
562
+ for await (const event of readSseJson<InteractionSseEvent>(response.body, options?.signal, sse =>
563
+ options?.onSseEvent?.({ event: sse.event, data: sse.data, raw: [...sse.raw] }, model),
564
+ )) {
565
+ if (event.error) {
566
+ throw new AIError.ProviderResponseError(event.error.message ?? "Google Interactions API stream error", {
567
+ provider: model.provider,
568
+ kind: "runtime",
569
+ });
570
+ }
571
+ if (event.metadata?.total_usage) applyInteractionUsage(model, output, event.metadata.total_usage);
572
+ if (event.event_type === "interaction.created") {
573
+ if (storeInteraction && event.interaction?.id) output.responseId = event.interaction.id;
574
+ } else if (event.event_type === "step.start" && event.index !== undefined && event.step) {
575
+ stepKinds.set(event.index, event.step.type);
576
+ const call = parseInteractionFunctionCall(event.step);
577
+ if (call) {
578
+ pendingToolCalls.set(event.index, pendingToolCallFromStep(call));
579
+ } else if (event.step.type === "thought") {
580
+ applyThinkingSignature(event.step.signature);
581
+ for (const item of event.step.summary ?? []) {
582
+ if (item.type === "text") emitThinking(item.text);
583
+ }
584
+ }
585
+ } else if (event.event_type === "step.delta" && event.index !== undefined && event.delta) {
586
+ const call = parseInteractionFunctionCall(event.delta);
587
+ if (call) {
588
+ pendingToolCalls.set(event.index, pendingToolCallFromStep(call));
589
+ } else if (event.delta.type === "text") {
590
+ if (stepKinds.get(event.index) === "thought") emitThinking(event.delta.text);
591
+ else emitText(event.delta.text);
592
+ } else if (event.delta.type === "thought_summary") {
593
+ if (event.delta.content?.type === "text") emitThinking(event.delta.content.text);
594
+ } else if (event.delta.type === "thought_signature") {
595
+ applyThinkingSignature(event.delta.signature);
596
+ } else if (event.delta.type === "arguments_delta") {
597
+ const pending = pendingToolCalls.get(event.index);
598
+ if (pending && event.delta.arguments) pending.argumentsText += event.delta.arguments;
599
+ }
600
+ } else if (event.event_type === "step.stop" && event.index !== undefined) {
601
+ const stepKind = stepKinds.get(event.index);
602
+ const pending = pendingToolCalls.get(event.index);
603
+ if (pending) {
604
+ emitPendingToolCall(pending);
605
+ pendingToolCalls.delete(event.index);
606
+ } else {
607
+ endOpenBlocks();
608
+ if (stepKind === "thought") pendingThinkingSignature = undefined;
609
+ }
610
+ stepKinds.delete(event.index);
611
+ } else if (event.event_type === "interaction.completed" || event.event_type === "interaction.complete") {
612
+ if (storeInteraction && event.interaction?.id) output.responseId = event.interaction.id;
613
+ if (event.interaction?.usage) applyInteractionUsage(model, output, event.interaction.usage);
614
+ for (const pending of pendingToolCalls.values()) emitPendingToolCall(pending);
615
+ pendingToolCalls.clear();
616
+ endOpenBlocks();
617
+ output.stopReason =
618
+ event.interaction?.status === "requires_action" ||
619
+ output.content.some(block => block.type === "toolCall")
620
+ ? "toolUse"
621
+ : "stop";
622
+ if (storeInteraction && state) state.lastInteractionId = output.responseId;
623
+ sawTerminal = true;
624
+ ensureStarted();
625
+ stream.push({ type: "done", reason: output.stopReason, message: output });
626
+ }
627
+ }
628
+ if (!sawTerminal) {
629
+ throw new AIError.ProviderResponseError("Google Interactions API stream ended without a terminal event", {
630
+ provider: model.provider,
631
+ kind: "incomplete-stream",
632
+ });
633
+ }
634
+ } catch (error) {
635
+ // Auto-selected Interactions degrades to `:streamGenerateContent` when no content has
636
+ // streamed yet and the failure means Interactions can't serve this request — the bearer
637
+ // credential couldn't be resolved (prepare threw) or the model/endpoint rejected it
638
+ // (HTTP 404/400). Provider 401/403/429/5xx still surface. Mirrors the OpenAI Responses
639
+ // `previous_response_id` fallback.
640
+ const unsupported = error instanceof AIError.GoogleApiError && (error.status === 404 || error.status === 400);
641
+ if (!started && args.fallback && !options?.signal?.aborted && (!prepared || unsupported)) {
642
+ for await (const event of args.fallback()) stream.push(event);
643
+ return;
644
+ }
645
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
646
+ output.errorMessage = error instanceof Error ? error.message : String(error);
647
+ stream.push({ type: "error", reason: output.stopReason, error: output });
648
+ }
649
+ })();
650
+
651
+ return stream;
652
+ }
653
+
654
+ function findAssistantInteractionAnchor(
655
+ context: Context,
656
+ interactionId: string,
657
+ provider: string,
658
+ ): InteractionAnchor | undefined {
659
+ for (let index = context.messages.length - 1; index >= 0; index -= 1) {
660
+ const message = context.messages[index];
661
+ if (message?.role === "assistant" && message.provider === provider && message.responseId === interactionId) {
662
+ return { id: interactionId, messageIndex: index };
663
+ }
664
+ }
665
+ return undefined;
666
+ }
667
+
668
+ function latestAssistantInteractionAnchor(context: Context, provider: string): InteractionAnchor | undefined {
669
+ for (let index = context.messages.length - 1; index >= 0; index -= 1) {
670
+ const message = context.messages[index];
671
+ if (message?.role === "assistant" && message.provider === provider && message.responseId) {
672
+ return { id: message.responseId, messageIndex: index };
673
+ }
674
+ }
675
+ return undefined;
676
+ }
677
+
678
+ function resolveInteractionAnchor(
679
+ context: Context,
680
+ explicitPreviousInteractionId: string | undefined,
681
+ state: GoogleInteractionsProviderSessionState | undefined,
682
+ provider: string,
683
+ ): InteractionAnchor {
684
+ if (explicitPreviousInteractionId !== undefined) {
685
+ return (
686
+ findAssistantInteractionAnchor(context, explicitPreviousInteractionId, provider) ?? {
687
+ id: explicitPreviousInteractionId,
688
+ }
689
+ );
690
+ }
691
+ const lineageAnchor = latestAssistantInteractionAnchor(context, provider);
692
+ if (lineageAnchor) return lineageAnchor;
693
+ if (state?.lastInteractionId)
694
+ return findAssistantInteractionAnchor(context, state.lastInteractionId, provider) ?? {};
695
+ return {};
696
+ }
697
+
698
+ /**
699
+ * Whether a model is served by the Gemini Interactions API. Interactions is a Gemini 3-era
700
+ * transport, so the catalog subset that supports it is Gemini 3.0+. Older Gemini and non-Gemini
701
+ * ids keep `:streamGenerateContent`, which covers the full catalog.
702
+ */
703
+ export function modelSupportsInteractions(model: Pick<Model, "id">): boolean {
704
+ const parsed = parseGeminiModel(model.id);
705
+ return parsed !== null && parsed.version.major >= 3;
706
+ }
707
+
708
+ /**
709
+ * Resolves whether a Google provider call should use Interactions and which lineage anchor to send.
710
+ *
711
+ * Precedence: explicit `useInteractionsApi: false` always wins (force generateContent); otherwise
712
+ * Interactions engages when explicitly requested, when continuing a stored interaction
713
+ * (`previousInteractionId`/assistant lineage/session state), or when `autoEligible` (the
714
+ * zero-config default for the capable model subset on the official endpoint). `auto` flags the
715
+ * last case for the caller — it is the only mode that wires up the generateContent fallback.
716
+ */
717
+ export function resolveInteractionDispatch(args: {
718
+ context: Context;
719
+ options: GoogleSharedStreamOptions | undefined;
720
+ provider: string;
721
+ autoEligible: boolean;
722
+ }): {
723
+ useInteractions: boolean;
724
+ auto: boolean;
725
+ anchor: InteractionAnchor;
726
+ state: GoogleInteractionsProviderSessionState | undefined;
727
+ } {
728
+ const explicitPreviousInteractionId = args.options?.previousInteractionId;
729
+ if (args.options?.storeInteraction === false && explicitPreviousInteractionId !== undefined) {
730
+ throw new AIError.ConfigurationError(
731
+ "Google Interactions API cannot combine storeInteraction:false with previousInteractionId.",
732
+ );
733
+ }
734
+ const explicitOptOut = args.options?.useInteractionsApi === false;
735
+ const explicitOptIn = args.options?.useInteractionsApi === true || explicitPreviousInteractionId !== undefined;
736
+ const storageEnabled = args.options?.storeInteraction !== false;
737
+ const existingState = storageEnabled
738
+ ? getGoogleInteractionsState(args.options?.providerSessionState, false)
739
+ : undefined;
740
+ const anchor = explicitOptOut
741
+ ? {}
742
+ : resolveInteractionAnchor(args.context, explicitPreviousInteractionId, existingState, args.provider);
743
+ const useInteractions = !explicitOptOut && (explicitOptIn || anchor.id !== undefined || args.autoEligible);
744
+ const auto = useInteractions && !explicitOptIn;
745
+ const interactionState =
746
+ useInteractions && storageEnabled
747
+ ? getGoogleInteractionsState(
748
+ args.options?.providerSessionState,
749
+ args.options?.providerSessionState !== undefined,
750
+ )
751
+ : undefined;
752
+ return { useInteractions, auto, anchor, state: interactionState };
753
+ }