agent-accelerator 0.5.9 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -16
- package/examples/prompts/SYSTEM_PROMPT.md +2 -0
- package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
- package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
- package/package.json +4 -1
- package/src/agent/agent.ts +447 -172
- package/src/agent/architecture.md +16 -12
- package/src/agent/config-blocks.ts +39 -12
- package/src/agent/context.ts +9 -3
- package/src/agent/defaults.ts +88 -0
- package/src/agent/delegation.ts +86 -28
- package/src/agent/loop.ts +474 -374
- package/src/agent/subagent.ts +1 -3
- package/src/agent/subagents-tool.ts +50 -19
- package/src/agent/turns.ts +3 -1
- package/src/agent/usage-limit.ts +92 -0
- package/src/agent/worker.ts +73 -30
- package/src/agent/workers.ts +126 -21
- package/src/architecture.md +17 -15
- package/src/index.ts +43 -19
- package/src/models/architecture.md +4 -3
- package/src/models/catalog-cache.ts +6 -5
- package/src/models/catalog.ts +39 -30
- package/src/prompts/additional-context.md +2 -0
- package/src/prompts/architecture.md +19 -0
- package/src/prompts/prompts.ts +81 -0
- package/src/prompts/reasoning-directive.md +5 -0
- package/src/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +4 -0
- package/src/prompts/structured-output.md +2 -0
- package/src/{agent/subagents.md → prompts/subagents-tool.md} +3 -0
- package/src/prompts/tool-bridge.md +1 -0
- package/src/prompts/tool-interrupted.md +1 -0
- package/src/prompts/tool-not-found.md +1 -0
- package/src/prompts/tool-repeat-blocked.md +1 -0
- package/src/prompts/tool-schema-error.md +1 -0
- package/src/prompts/tool-truncated-arguments.md +1 -0
- package/src/prompts/worker-failed.md +1 -0
- package/src/prompts/worker-replied.md +2 -0
- package/src/providers/architecture.md +10 -8
- package/src/providers/google.ts +228 -69
- package/src/providers/openai-compat.ts +74 -56
- package/src/providers/openai.ts +64 -41
- package/src/providers/openrouter.ts +270 -48
- package/src/providers/registry.ts +6 -21
- package/src/providers/shared.ts +87 -33
- package/src/providers.ts +115 -124
- package/src/scheduler/architecture.md +8 -7
- package/src/scheduler/scheduler.ts +226 -60
- package/src/scheduler/store.ts +23 -10
- package/src/scheduler/time.ts +17 -3
- package/src/scheduler/tool.ts +38 -16
- package/src/session/architecture.md +7 -6
- package/src/session/journal.ts +39 -12
- package/src/session/store.ts +126 -61
- package/src/session/usage.ts +58 -9
- package/src/streaming/architecture.md +4 -3
- package/src/streaming/event-stream.ts +26 -12
- package/src/telemetry.ts +6 -2
- package/src/tools/architecture.md +9 -7
- package/src/tools/executor.ts +109 -62
- package/src/tools/schema.ts +10 -6
- package/src/tools/subagent-result.ts +6 -1
- package/src/tools/tool-content.ts +32 -0
- package/src/tools/tool.ts +9 -11
- package/src/types/agent.ts +37 -22
- package/src/types/architecture.md +11 -10
- package/src/types/core.ts +5 -9
- package/src/types/errors.ts +26 -4
- package/src/types/message.ts +7 -0
- package/src/types/model.ts +4 -5
- package/src/types/scheduler.ts +4 -4
- package/src/types/tool.ts +14 -2
- package/src/update-models.ts +5 -5
- package/src/utils/architecture.md +16 -15
- package/src/utils/cache.ts +1 -3
- package/src/utils/durable.ts +17 -5
- package/src/utils/env.ts +9 -7
- package/src/utils/errors.ts +9 -17
- package/src/utils/headers.ts +3 -10
- package/src/utils/media.ts +23 -8
- package/src/utils/raw-capture.ts +9 -2
- package/src/utils/retry.ts +15 -3
- package/src/utils/safe-io.ts +29 -7
- package/src/utils/serialization.ts +1 -4
- package/src/utils/session.ts +1 -2
package/src/providers/google.ts
CHANGED
|
@@ -29,7 +29,14 @@ import type {
|
|
|
29
29
|
ProviderGenerateResult,
|
|
30
30
|
ProviderRawData,
|
|
31
31
|
} from "../types/model.ts";
|
|
32
|
-
import type {
|
|
32
|
+
import type {
|
|
33
|
+
ProviderContext,
|
|
34
|
+
ContentPart,
|
|
35
|
+
ProviderSteps,
|
|
36
|
+
ToolResultPart,
|
|
37
|
+
MediaResolution,
|
|
38
|
+
VideoProcessing,
|
|
39
|
+
} from "../types/message.ts";
|
|
33
40
|
import type { StandardToolDeclaration, ToolCallRecord } from "../types/tool.ts";
|
|
34
41
|
import type { TokenUsage } from "../types/core.ts";
|
|
35
42
|
import { AssistantMessageEventStream } from "../streaming/event-stream.ts";
|
|
@@ -53,7 +60,6 @@ import {
|
|
|
53
60
|
combinedSignal,
|
|
54
61
|
timeoutFor,
|
|
55
62
|
hasAuthHeader,
|
|
56
|
-
clampToolCallId,
|
|
57
63
|
openStreamResponse,
|
|
58
64
|
retryOptionsFrom,
|
|
59
65
|
headersToRecord,
|
|
@@ -85,19 +91,70 @@ import {
|
|
|
85
91
|
// ---------------------------------------------------------------------------
|
|
86
92
|
|
|
87
93
|
/** Image MIME types Google accepts inline. */
|
|
88
|
-
export const GOOGLE_IMAGE_MIME = [
|
|
94
|
+
export const GOOGLE_IMAGE_MIME = [
|
|
95
|
+
"image/png",
|
|
96
|
+
"image/jpeg",
|
|
97
|
+
"image/webp",
|
|
98
|
+
"image/heic",
|
|
99
|
+
"image/heif",
|
|
100
|
+
"image/gif",
|
|
101
|
+
"image/bmp",
|
|
102
|
+
"image/tiff",
|
|
103
|
+
] as const;
|
|
89
104
|
/** Audio MIME types Google accepts inline (audio is never fetched by URL). */
|
|
90
|
-
export const GOOGLE_AUDIO_MIME = [
|
|
105
|
+
export const GOOGLE_AUDIO_MIME = [
|
|
106
|
+
"audio/wav",
|
|
107
|
+
"audio/mp3",
|
|
108
|
+
"audio/aiff",
|
|
109
|
+
"audio/aac",
|
|
110
|
+
"audio/ogg",
|
|
111
|
+
"audio/flac",
|
|
112
|
+
"audio/mpeg",
|
|
113
|
+
"audio/m4a",
|
|
114
|
+
"audio/l16",
|
|
115
|
+
"audio/opus",
|
|
116
|
+
"audio/alaw",
|
|
117
|
+
"audio/mulaw",
|
|
118
|
+
"audio/webm",
|
|
119
|
+
] as const;
|
|
91
120
|
/** Video MIME types Google accepts inline. */
|
|
92
|
-
export const GOOGLE_VIDEO_MIME = [
|
|
121
|
+
export const GOOGLE_VIDEO_MIME = [
|
|
122
|
+
"video/mp4",
|
|
123
|
+
"video/mpeg",
|
|
124
|
+
"video/mpg",
|
|
125
|
+
"video/mov",
|
|
126
|
+
"video/avi",
|
|
127
|
+
"video/x-flv",
|
|
128
|
+
"video/webm",
|
|
129
|
+
"video/wmv",
|
|
130
|
+
"video/3gpp",
|
|
131
|
+
] as const;
|
|
93
132
|
/** Document MIME types Google accepts (the only two). */
|
|
94
133
|
export const GOOGLE_DOCUMENT_MIME = ["application/pdf", "text/csv"] as const;
|
|
95
134
|
/** MIME types Google fetches itself from a public `https://` URL. */
|
|
96
135
|
export const GOOGLE_URL_MIME = [
|
|
97
|
-
"text/html",
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
136
|
+
"text/html",
|
|
137
|
+
"text/css",
|
|
138
|
+
"text/plain",
|
|
139
|
+
"text/xml",
|
|
140
|
+
"text/csv",
|
|
141
|
+
"text/rtf",
|
|
142
|
+
"text/javascript",
|
|
143
|
+
"application/json",
|
|
144
|
+
"application/pdf",
|
|
145
|
+
"image/bmp",
|
|
146
|
+
"image/jpeg",
|
|
147
|
+
"image/png",
|
|
148
|
+
"image/webp",
|
|
149
|
+
"video/mp4",
|
|
150
|
+
"video/mpeg",
|
|
151
|
+
"video/quicktime",
|
|
152
|
+
"video/avi",
|
|
153
|
+
"video/x-flv",
|
|
154
|
+
"video/mpg",
|
|
155
|
+
"video/webm",
|
|
156
|
+
"video/wmv",
|
|
157
|
+
"video/3gpp",
|
|
101
158
|
] as const;
|
|
102
159
|
/** Common MIME spellings mapped to Google's inline names. */
|
|
103
160
|
export const GOOGLE_INLINE_MIME_ALIASES: Readonly<Record<string, string>> = {
|
|
@@ -106,7 +163,8 @@ export const GOOGLE_INLINE_MIME_ALIASES: Readonly<Record<string, string>> = {
|
|
|
106
163
|
"audio/wave": "audio/wav",
|
|
107
164
|
"image/jpg": "image/jpeg",
|
|
108
165
|
};
|
|
109
|
-
// Documented size limits.
|
|
166
|
+
// Documented size limits (file-input-methods.md). `assertGoogleInlineSizes` below checks them, but it is NOT called yet:
|
|
167
|
+
// the owner is not sure how Files API uploads should be handled, so oversized inputs still get Google's own error for now.
|
|
110
168
|
/** Max inline payload per request (bytes). */
|
|
111
169
|
export const GOOGLE_MAX_INLINE_REQUEST_BYTES = 100 * 1024 * 1024;
|
|
112
170
|
/** Max inline PDF size (bytes). */
|
|
@@ -167,14 +225,22 @@ const INLINE_MIME: Record<MediaBlockType, readonly string[]> = {
|
|
|
167
225
|
document: GOOGLE_DOCUMENT_MIME,
|
|
168
226
|
};
|
|
169
227
|
|
|
170
|
-
|
|
228
|
+
// A Google Files API link on either API version (`/v1/files/<id>` or `/v1beta/files/<id>`); the host is exact, so no other URL counts.
|
|
229
|
+
const FILES_API_URL = /^https:\/\/generativelanguage\.googleapis\.com\/v1(?:beta)?\/files\/[^/\s]+/;
|
|
171
230
|
const YOUTUBE_URL = /^https:\/\/(www\.|m\.)?(youtube\.com|youtu\.be)\//i;
|
|
172
231
|
|
|
173
232
|
// ---------------------------------------------------------------------------
|
|
174
233
|
// Interactions wire shapes (subset used by this adapter)
|
|
175
234
|
// ---------------------------------------------------------------------------
|
|
176
235
|
|
|
177
|
-
type MediaBlock = {
|
|
236
|
+
type MediaBlock = {
|
|
237
|
+
type: MediaBlockType;
|
|
238
|
+
mime_type?: string;
|
|
239
|
+
data?: string;
|
|
240
|
+
uri?: string;
|
|
241
|
+
resolution?: MediaResolution;
|
|
242
|
+
processing?: VideoProcessing;
|
|
243
|
+
};
|
|
178
244
|
type InteractionContentBlock = { type: "text"; text: string } | MediaBlock;
|
|
179
245
|
|
|
180
246
|
type InteractionInputStep =
|
|
@@ -317,7 +383,10 @@ function liveChainFor(sessionId: string | undefined, options?: ProviderRequestOp
|
|
|
317
383
|
* below the previous turn's total (Rule 3: a caller dropped the chain without
|
|
318
384
|
* rehydrating history). Never throws; observability-only.
|
|
319
385
|
*/
|
|
320
|
-
function checkTurnTokens(
|
|
386
|
+
function checkTurnTokens(
|
|
387
|
+
sessionId: string | undefined,
|
|
388
|
+
usage: { inputTokens: number; outputTokens: number; cachedTokens?: number }
|
|
389
|
+
): void {
|
|
321
390
|
try {
|
|
322
391
|
if (!sessionId) return;
|
|
323
392
|
const total = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
|
|
@@ -409,9 +478,7 @@ function toGoogleTools(tools?: StandardToolDeclaration[]): Array<Record<string,
|
|
|
409
478
|
type: "function",
|
|
410
479
|
name: t.name,
|
|
411
480
|
description: t.description,
|
|
412
|
-
parameters: stripSchemaForGoogle(
|
|
413
|
-
(t.parameters || { type: "object", properties: {} }) as Record<string, unknown>
|
|
414
|
-
),
|
|
481
|
+
parameters: stripSchemaForGoogle((t.parameters || { type: "object", properties: {} }) as Record<string, unknown>),
|
|
415
482
|
}));
|
|
416
483
|
}
|
|
417
484
|
|
|
@@ -442,8 +509,10 @@ function uriBlock(type: MediaBlockType, value: string, mimeType?: string): Media
|
|
|
442
509
|
const withMime = (mime?: string): MediaBlock => ({ type, uri: value, ...(mime ? { mime_type: mime } : {}) });
|
|
443
510
|
if (type === "video" && YOUTUBE_URL.test(value)) return withMime(explicit);
|
|
444
511
|
const mime = explicit ?? bareMime(inferMimeType(value, ""));
|
|
445
|
-
if (
|
|
446
|
-
return value.startsWith("https://") && mime && (GOOGLE_URL_MIME as readonly string[]).includes(mime)
|
|
512
|
+
if (FILES_API_URL.test(value) || value.startsWith("gs://")) return withMime(mime);
|
|
513
|
+
return value.startsWith("https://") && mime && (GOOGLE_URL_MIME as readonly string[]).includes(mime)
|
|
514
|
+
? withMime(mime)
|
|
515
|
+
: undefined;
|
|
447
516
|
}
|
|
448
517
|
|
|
449
518
|
// Inlines a media part as base64 under Google's MIME name, failing before the network on an unsupported type; in: any media value (bytes, data URL, path, http URL), out: contentPartsToBlocks.
|
|
@@ -452,7 +521,8 @@ async function inlineBlock(type: MediaBlockType, value: MediaValue, mimeType?: s
|
|
|
452
521
|
const raw = bareMime(norm.mimeType) ?? "";
|
|
453
522
|
const mime = GOOGLE_INLINE_MIME_ALIASES[raw] ?? raw;
|
|
454
523
|
if (!INLINE_MIME[type].includes(mime)) {
|
|
455
|
-
const what =
|
|
524
|
+
const what =
|
|
525
|
+
type === "document" ? "Google reads only PDF and CSV documents" : `Google does not accept ${type} type "${mime}"`;
|
|
456
526
|
throw new ValidationError(
|
|
457
527
|
`[Agent Accelerator] ${what} (got "${mime}"; supported: ${INLINE_MIME[type].join(", ")}). Convert the file or pass the correct mimeType.`
|
|
458
528
|
);
|
|
@@ -475,11 +545,63 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<InteractionCo
|
|
|
475
545
|
const block = uri ?? (await inlineBlock(type, value, part.mimeType));
|
|
476
546
|
// Per-item detail level (image and video blocks only, api-ref `MediaResolution`).
|
|
477
547
|
const resolution = part.type === "image" || part.type === "video" ? part.resolution : undefined;
|
|
478
|
-
|
|
548
|
+
// Per-video processing mode (`static` | `agentic`, api-ref `MediaProcessing`): the model may then add `processing_call` / `processing_result` steps, which the raw-steps replay keeps like any other step.
|
|
549
|
+
const processing = part.type === "video" ? part.processing : undefined;
|
|
550
|
+
if (processing !== undefined && processing !== "static" && processing !== "agentic")
|
|
551
|
+
throw new ValidationError(
|
|
552
|
+
`[Agent Accelerator] Invalid video processing ${JSON.stringify(processing)}: expected "static" or "agentic".`
|
|
553
|
+
);
|
|
554
|
+
blocks.push({ ...block, ...(resolution ? { resolution } : {}), ...(processing ? { processing } : {}) });
|
|
479
555
|
}
|
|
480
556
|
return blocks;
|
|
481
557
|
}
|
|
482
558
|
|
|
559
|
+
// Decoded size in bytes of a base64 string, without decoding it; in: assertGoogleInlineSizes, out: a byte count.
|
|
560
|
+
function base64Bytes(b64: string): number {
|
|
561
|
+
const clean = b64.replace(/\s/g, "");
|
|
562
|
+
const padding = clean.endsWith("==") ? 2 : clean.endsWith("=") ? 1 : 0;
|
|
563
|
+
return Math.max(0, Math.floor((clean.length * 3) / 4) - padding);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Every inline media block (`data` + `mime_type`) found anywhere in a request `input`, including blocks inside steps and function results; in: assertGoogleInlineSizes, out: the blocks.
|
|
567
|
+
function inlineBlocks(
|
|
568
|
+
value: unknown,
|
|
569
|
+
out: Array<{ mime?: string; bytes: number }> = []
|
|
570
|
+
): Array<{ mime?: string; bytes: number }> {
|
|
571
|
+
if (Array.isArray(value)) {
|
|
572
|
+
for (const v of value) inlineBlocks(v, out);
|
|
573
|
+
} else if (value && typeof value === "object") {
|
|
574
|
+
const o = value as Record<string, unknown>;
|
|
575
|
+
if (typeof o["data"] === "string" && typeof o["mime_type"] === "string")
|
|
576
|
+
out.push({ mime: bareMime(o["mime_type"]), bytes: base64Bytes(o["data"]) });
|
|
577
|
+
else for (const v of Object.values(o)) inlineBlocks(v, out);
|
|
578
|
+
}
|
|
579
|
+
return out;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Fails before the network when the inline media of one request is over Google's documented limits: 100 MB in total and
|
|
584
|
+
* 50 MB for each PDF (larger files go through the Files API). Counts decoded bytes, over the whole `input` (the full
|
|
585
|
+
* history on a reseed). NOT called by the adapter yet (see the call sites in `generate` and `stream`).
|
|
586
|
+
*/
|
|
587
|
+
export function assertGoogleInlineSizes(input: unknown, modelId: string): void {
|
|
588
|
+
const blocks = inlineBlocks(input);
|
|
589
|
+
const mb = (n: number): string => `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
590
|
+
for (const b of blocks) {
|
|
591
|
+
if (b.mime === "application/pdf" && b.bytes > GOOGLE_MAX_INLINE_PDF_BYTES) {
|
|
592
|
+
throw new ValidationError(
|
|
593
|
+
`[Agent Accelerator] [google/${modelId}] A PDF of ${mb(b.bytes)} is over Google's ${mb(GOOGLE_MAX_INLINE_PDF_BYTES)} limit for inline PDFs. Upload it with the Files API and pass its link instead.`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const total = blocks.reduce((n, b) => n + b.bytes, 0);
|
|
598
|
+
if (total > GOOGLE_MAX_INLINE_REQUEST_BYTES) {
|
|
599
|
+
throw new ValidationError(
|
|
600
|
+
`[Agent Accelerator] [google/${modelId}] The inline media of this request is ${mb(total)}, over Google's ${mb(GOOGLE_MAX_INLINE_REQUEST_BYTES)} limit per request. Upload the large files with the Files API and pass their links instead.`
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
483
605
|
// Keeps a turn's model-generated steps exactly as received (input echoes and empty signatures dropped); in: REST `steps` or assembled stream steps, out: ProviderGenerateResult.providerSteps.
|
|
484
606
|
function replaySteps(steps: unknown[] | undefined): ProviderSteps | undefined {
|
|
485
607
|
const kept = (steps ?? [])
|
|
@@ -515,13 +637,14 @@ function resultToText(result: unknown): string {
|
|
|
515
637
|
return safeStringify(result);
|
|
516
638
|
}
|
|
517
639
|
|
|
518
|
-
// Builds the documented `function_result` step (`is_error` only when the tool failed); in: a canonical tool_result part, out: chained or full-history `input`.
|
|
519
|
-
function functionResultStep(part: ToolResultPart): InteractionInputStep {
|
|
640
|
+
// Builds the documented `function_result` step (`is_error` only when the tool failed); text first, then the media the tool returned with `toolContent` (Gemini 3 accepts image and document blocks in `result`); in: a canonical tool_result part, out: chained or full-history `input`.
|
|
641
|
+
async function functionResultStep(part: ToolResultPart): Promise<InteractionInputStep> {
|
|
642
|
+
const media = part.parts && part.parts.length > 0 ? await contentPartsToBlocks(part.parts) : [];
|
|
520
643
|
return {
|
|
521
644
|
type: "function_result",
|
|
522
645
|
name: part.name,
|
|
523
646
|
call_id: part.id,
|
|
524
|
-
result: [{ type: "text", text: resultToText(part.result) }],
|
|
647
|
+
result: [{ type: "text", text: resultToText(part.result) }, ...media],
|
|
525
648
|
...(part.isError ? { is_error: true } : {}),
|
|
526
649
|
};
|
|
527
650
|
}
|
|
@@ -534,7 +657,9 @@ function functionResultStep(part: ToolResultPart): InteractionInputStep {
|
|
|
534
657
|
* behind them. Sending only the last message would drop the tool results and
|
|
535
658
|
* leave the chained interaction waiting on its function calls.
|
|
536
659
|
*/
|
|
537
|
-
async function latestTurnInput(
|
|
660
|
+
async function latestTurnInput(
|
|
661
|
+
context: ProviderContext
|
|
662
|
+
): Promise<string | InteractionContentBlock[] | InteractionInputStep[]> {
|
|
538
663
|
const messages = context.messages;
|
|
539
664
|
let lastAssistant = -1;
|
|
540
665
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -563,7 +688,7 @@ async function latestTurnInput(context: ProviderContext): Promise<string | Inter
|
|
|
563
688
|
if (!Array.isArray(m.content)) continue;
|
|
564
689
|
for (const part of m.content) {
|
|
565
690
|
if (part.type === "tool_result") {
|
|
566
|
-
steps.push(functionResultStep(part));
|
|
691
|
+
steps.push(await functionResultStep(part));
|
|
567
692
|
}
|
|
568
693
|
}
|
|
569
694
|
} else if (m.role === "user") {
|
|
@@ -617,10 +742,11 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
617
742
|
const sig = shouldEchoSignature(partProvider ?? msgProvider, "google")
|
|
618
743
|
? part.thoughtSignature || m.thoughtSignature || ""
|
|
619
744
|
: "";
|
|
620
|
-
const thoughtStep: { type: "thought"; signature?: string; summary: Array<{ type: string; text?: string }> } =
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
745
|
+
const thoughtStep: { type: "thought"; signature?: string; summary: Array<{ type: string; text?: string }> } =
|
|
746
|
+
{
|
|
747
|
+
type: "thought",
|
|
748
|
+
summary: [{ type: "text", text: part.thinking }],
|
|
749
|
+
};
|
|
624
750
|
if (sig) thoughtStep.signature = sig;
|
|
625
751
|
steps.push(thoughtStep);
|
|
626
752
|
} else if (part.type === "tool_call") {
|
|
@@ -652,7 +778,7 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
652
778
|
if (!Array.isArray(m.content)) continue;
|
|
653
779
|
for (const part of m.content) {
|
|
654
780
|
if (part.type === "tool_result") {
|
|
655
|
-
steps.push(functionResultStep(part));
|
|
781
|
+
steps.push(await functionResultStep(part));
|
|
656
782
|
}
|
|
657
783
|
}
|
|
658
784
|
}
|
|
@@ -733,11 +859,7 @@ function parseInteraction(
|
|
|
733
859
|
// truncated marker); absent args mean a zero-argument call.
|
|
734
860
|
const rawArgs: unknown = step.arguments;
|
|
735
861
|
const args =
|
|
736
|
-
rawArgs && typeof rawArgs === "object"
|
|
737
|
-
? rawArgs
|
|
738
|
-
: typeof rawArgs === "string"
|
|
739
|
-
? parseArguments(rawArgs)
|
|
740
|
-
: {};
|
|
862
|
+
rawArgs && typeof rawArgs === "object" ? rawArgs : typeof rawArgs === "string" ? parseArguments(rawArgs) : {};
|
|
741
863
|
toolCalls.push({
|
|
742
864
|
id: step.id || newToolCallId(),
|
|
743
865
|
name: step.name || "unknown",
|
|
@@ -815,12 +937,11 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
815
937
|
// only signatures return and canonical `thinking` would always be empty.
|
|
816
938
|
// Enabled whenever thinking is active — including the default/dynamic
|
|
817
939
|
// case where no explicit level is sent — and off only when disabled.
|
|
818
|
-
const thinkingActive =
|
|
819
|
-
options?.thinking?.enabled !== false && (options?.thinking?.level ?? "dynamic") !== "none";
|
|
940
|
+
const thinkingActive = options?.thinking?.enabled !== false && (options?.thinking?.level ?? "dynamic") !== "none";
|
|
820
941
|
if (thinkingActive) generationConfig["thinking_summaries"] = "auto";
|
|
821
942
|
if (toolChoice) generationConfig["tool_choice"] = toolChoice;
|
|
822
|
-
// max_output_tokens only
|
|
823
|
-
Object.assign(generationConfig, mapGenerationParamsToGoogle(options
|
|
943
|
+
// max_output_tokens only.
|
|
944
|
+
Object.assign(generationConfig, mapGenerationParamsToGoogle(options));
|
|
824
945
|
|
|
825
946
|
const body: InteractionRequestBody = {
|
|
826
947
|
model: modelId,
|
|
@@ -834,19 +955,26 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
834
955
|
let outputEmulated = false;
|
|
835
956
|
let outputSpec: import("../types/model.ts").StructuredOutputSpec | undefined;
|
|
836
957
|
try {
|
|
837
|
-
outputSpec = normalizeStructuredOutput(
|
|
958
|
+
outputSpec = normalizeStructuredOutput(
|
|
959
|
+
options?.output ?? (options as { outputSchema?: unknown })?.outputSchema
|
|
960
|
+
);
|
|
838
961
|
} catch {
|
|
839
962
|
outputSpec = undefined;
|
|
840
963
|
}
|
|
841
964
|
let effectiveSystemPrompt = context.systemPrompt;
|
|
842
965
|
if (outputSpec && !modelSupportsStructuredOutput("google", modelId)) {
|
|
843
966
|
outputEmulated = true;
|
|
844
|
-
effectiveSystemPrompt = augmentSystemPromptWithStructuredOutput(
|
|
967
|
+
effectiveSystemPrompt = augmentSystemPromptWithStructuredOutput(
|
|
968
|
+
context.systemPrompt,
|
|
969
|
+
outputSpec,
|
|
970
|
+
options?.prompts?.structuredOutput
|
|
971
|
+
);
|
|
845
972
|
emitProviderWarning({
|
|
846
973
|
provider: "google",
|
|
847
974
|
capability: "structured output",
|
|
848
975
|
requested: `${outputSpec.name} (google/${modelId})`,
|
|
849
|
-
reason:
|
|
976
|
+
reason:
|
|
977
|
+
"the catalog reports structured_output: false for this model, so native response_format is unsupported.",
|
|
850
978
|
fallback: "emulated JSON instruction + client validation (no response_format is sent)",
|
|
851
979
|
});
|
|
852
980
|
}
|
|
@@ -927,7 +1055,10 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
927
1055
|
apiKey: string,
|
|
928
1056
|
signal?: AbortSignal
|
|
929
1057
|
): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
|
|
930
|
-
const { signal: effective, cleanup } = combinedSignal(
|
|
1058
|
+
const { signal: effective, cleanup } = combinedSignal(
|
|
1059
|
+
signal,
|
|
1060
|
+
timeoutFor(options as { timeoutMs?: number } | undefined)
|
|
1061
|
+
);
|
|
931
1062
|
try {
|
|
932
1063
|
const res = await (options?.fetch ?? fetch)(url, this.requestInit(method, body, options, apiKey, effective));
|
|
933
1064
|
const text = await res.text();
|
|
@@ -944,7 +1075,6 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
944
1075
|
// Throws a concise provider error (status, code, Retry-After, key hint) for a non-2xx reply; in: every Google HTTP call, out: nothing or a thrown error.
|
|
945
1076
|
private throwIfError(
|
|
946
1077
|
status: number,
|
|
947
|
-
statusText: string,
|
|
948
1078
|
url: string,
|
|
949
1079
|
bodyText: string,
|
|
950
1080
|
modelId: string,
|
|
@@ -989,9 +1119,14 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
989
1119
|
modelId: string
|
|
990
1120
|
): Promise<InteractionReply> {
|
|
991
1121
|
const res = await this.doFetch(url, method, body, options, apiKey, options?.signal);
|
|
992
|
-
this.throwIfError(res.status,
|
|
1122
|
+
this.throwIfError(res.status, url, res.text, modelId, res.headers);
|
|
993
1123
|
try {
|
|
994
|
-
return {
|
|
1124
|
+
return {
|
|
1125
|
+
status: res.status,
|
|
1126
|
+
statusText: res.statusText,
|
|
1127
|
+
headers: res.headers,
|
|
1128
|
+
interaction: JSON.parse(res.text) as InteractionObject,
|
|
1129
|
+
};
|
|
995
1130
|
} catch {
|
|
996
1131
|
throw toConciseProviderError(
|
|
997
1132
|
Object.assign(new Error("Invalid JSON response from Google Interactions API"), {
|
|
@@ -1006,7 +1141,12 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1006
1141
|
}
|
|
1007
1142
|
|
|
1008
1143
|
// Fire-and-forget cancel so an aborted background run stops billing; in: an aborted generate/stream, out: POST {id}/cancel.
|
|
1009
|
-
private cancelInteraction(
|
|
1144
|
+
private cancelInteraction(
|
|
1145
|
+
baseUrl: string,
|
|
1146
|
+
id: string,
|
|
1147
|
+
options: ProviderRequestOptions | undefined,
|
|
1148
|
+
apiKey: string
|
|
1149
|
+
): void {
|
|
1010
1150
|
void this.doFetch(`${baseUrl}/interactions/${id}/cancel`, "POST", undefined, options, apiKey).catch(() => {});
|
|
1011
1151
|
}
|
|
1012
1152
|
|
|
@@ -1035,6 +1175,8 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1035
1175
|
const sessionId = options?.sessionId;
|
|
1036
1176
|
const tools = options?.tools as StandardToolDeclaration[] | undefined;
|
|
1037
1177
|
let body = await this.buildBody(clean, context, options, sessionId, tools, false);
|
|
1178
|
+
// Size limits are implemented but switched off for now (the owner is unsure about Files API handling yet):
|
|
1179
|
+
// assertGoogleInlineSizes(body.input, clean);
|
|
1038
1180
|
|
|
1039
1181
|
const makeRawRequest = (b: InteractionRequestBody): ProviderRawData["request"] => ({
|
|
1040
1182
|
url,
|
|
@@ -1060,7 +1202,10 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1060
1202
|
try {
|
|
1061
1203
|
for (let poll = 0; isRunning(reply.interaction.status); poll++) {
|
|
1062
1204
|
await sleep(this.pollDelaysMs[poll === 0 ? 0 : 1], options?.signal);
|
|
1063
|
-
reply = await withRetries(
|
|
1205
|
+
reply = await withRetries(
|
|
1206
|
+
() => this.fetchInteraction(`${url}/${id}`, "GET", undefined, options, apiKey, clean),
|
|
1207
|
+
retryOptions
|
|
1208
|
+
);
|
|
1064
1209
|
}
|
|
1065
1210
|
return reply;
|
|
1066
1211
|
} catch (err) {
|
|
@@ -1071,7 +1216,10 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1071
1216
|
|
|
1072
1217
|
// Create (retried as a whole), wait if it runs in the background, then record the chain and map the result.
|
|
1073
1218
|
const run = async (): Promise<ProviderGenerateResult> => {
|
|
1074
|
-
let reply = await withRetries(
|
|
1219
|
+
let reply = await withRetries(
|
|
1220
|
+
() => this.fetchInteraction(url, "POST", body, options, apiKey, clean),
|
|
1221
|
+
retryOptions
|
|
1222
|
+
);
|
|
1075
1223
|
if (body.background) reply = await waitForCompletion(reply);
|
|
1076
1224
|
const { interaction } = reply;
|
|
1077
1225
|
if (interaction.id && sessionId && body.store !== false) {
|
|
@@ -1081,12 +1229,10 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1081
1229
|
lastTurnTotals.delete(sessionId);
|
|
1082
1230
|
}
|
|
1083
1231
|
noteProviderTurn(sessionId, "google");
|
|
1084
|
-
const parsed = parseInteraction(
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
{ request: rawRequest, response: { status: reply.status, statusText: reply.statusText, headers: reply.headers, body: interaction } }
|
|
1089
|
-
);
|
|
1232
|
+
const parsed = parseInteraction(interaction, clean, Date.now() - startTime, {
|
|
1233
|
+
request: rawRequest,
|
|
1234
|
+
response: { status: reply.status, statusText: reply.statusText, headers: reply.headers, body: interaction },
|
|
1235
|
+
});
|
|
1090
1236
|
checkTurnTokens(sessionId, parsed.usage);
|
|
1091
1237
|
return parsed;
|
|
1092
1238
|
};
|
|
@@ -1143,6 +1289,8 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1143
1289
|
const sessionId = options?.sessionId;
|
|
1144
1290
|
const tools = options?.tools as StandardToolDeclaration[] | undefined;
|
|
1145
1291
|
const body = await this.buildBody(clean, context, options, sessionId, tools, true);
|
|
1292
|
+
// Size limits are implemented but switched off for now (the owner is unsure about Files API handling yet):
|
|
1293
|
+
// assertGoogleInlineSizes(body.input, clean);
|
|
1146
1294
|
evictSessionId = sessionId;
|
|
1147
1295
|
usedChain = body.previous_interaction_id !== undefined;
|
|
1148
1296
|
|
|
@@ -1165,9 +1313,16 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1165
1313
|
// (429/5xx/network) are retried before the first byte; a live stream is never retried.
|
|
1166
1314
|
const open = (target: string, method: "POST" | "GET"): Promise<Response> =>
|
|
1167
1315
|
openStreamResponse({
|
|
1168
|
-
fetchOnce: () =>
|
|
1169
|
-
|
|
1170
|
-
|
|
1316
|
+
fetchOnce: () =>
|
|
1317
|
+
withAbortTimer(linked, streamTimeoutMs, () =>
|
|
1318
|
+
(options?.fetch ?? fetch)(
|
|
1319
|
+
target,
|
|
1320
|
+
this.requestInit(method, method === "POST" ? body : undefined, options, apiKey, linked.signal)
|
|
1321
|
+
)
|
|
1322
|
+
),
|
|
1323
|
+
onHttpError: (r, text) => this.throwIfError(r.status, target, text, clean, headersToRecord(r.headers)),
|
|
1324
|
+
noBodyError: () =>
|
|
1325
|
+
toConciseProviderError(new Error("Google streaming response had no body"), "google", clean),
|
|
1171
1326
|
retry: retryOptionsFrom(options),
|
|
1172
1327
|
signal: linked.signal,
|
|
1173
1328
|
label: { providerId: "google", modelId: clean },
|
|
@@ -1177,7 +1332,11 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1177
1332
|
res.headers.forEach((v, k) => {
|
|
1178
1333
|
responseHeaders[k] = v;
|
|
1179
1334
|
});
|
|
1180
|
-
const responseMeta: Record<string, unknown> & {
|
|
1335
|
+
const responseMeta: Record<string, unknown> & {
|
|
1336
|
+
status: number;
|
|
1337
|
+
statusText: string;
|
|
1338
|
+
headers: Record<string, string>;
|
|
1339
|
+
} = { status: res.status, statusText: res.statusText, headers: responseHeaders };
|
|
1181
1340
|
eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
|
|
1182
1341
|
|
|
1183
1342
|
let reader: StreamReader | undefined;
|
|
@@ -1286,7 +1445,8 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1286
1445
|
thinking += t;
|
|
1287
1446
|
eventStream.push({ type: "thinking_delta", thinkingDelta: t, partialThinking: thinking });
|
|
1288
1447
|
}
|
|
1289
|
-
if (!signature && typeof step["signature"] === "string" && step["signature"])
|
|
1448
|
+
if (!signature && typeof step["signature"] === "string" && step["signature"])
|
|
1449
|
+
signature = step["signature"];
|
|
1290
1450
|
} else if (step["type"] === "function_call") {
|
|
1291
1451
|
const args = step["arguments"];
|
|
1292
1452
|
const startArgs = typeof args === "string" ? args : args ? JSON.stringify(args) : "";
|
|
@@ -1343,7 +1503,8 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1343
1503
|
break;
|
|
1344
1504
|
}
|
|
1345
1505
|
if (chunk.done) break;
|
|
1346
|
-
for (const m of parser.feed(decoder.decode(chunk.value, { stream: true })))
|
|
1506
|
+
for (const m of parser.feed(decoder.decode(chunk.value, { stream: true })))
|
|
1507
|
+
handleMessage(m.event, m.data);
|
|
1347
1508
|
}
|
|
1348
1509
|
for (const m of parser.flush()) handleMessage(m.event, m.data);
|
|
1349
1510
|
} finally {
|
|
@@ -1383,7 +1544,8 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1383
1544
|
}
|
|
1384
1545
|
|
|
1385
1546
|
if (sessionId) {
|
|
1386
|
-
if (responseId && body.store !== false)
|
|
1547
|
+
if (responseId && body.store !== false)
|
|
1548
|
+
setChain(sessionId, { interactionId: responseId, model: clean, at: Date.now() });
|
|
1387
1549
|
else if (body.store === false) {
|
|
1388
1550
|
interactionChains.delete(sessionId);
|
|
1389
1551
|
lastTurnTotals.delete(sessionId);
|
|
@@ -1402,7 +1564,8 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1402
1564
|
const cleanThinkingEarly = thinking.replace(/\n{3,}/g, "\n\n").trim();
|
|
1403
1565
|
const ctEntry = Object.entries(responseHeaders).find(([k]) => k.toLowerCase() === "content-type");
|
|
1404
1566
|
const isSSEPayload =
|
|
1405
|
-
(typeof ctEntry?.[1] === "string" &&
|
|
1567
|
+
(typeof ctEntry?.[1] === "string" &&
|
|
1568
|
+
((ctEntry[1] as string).includes("event-stream") || (ctEntry[1] as string).startsWith("text/"))) ||
|
|
1406
1569
|
framesSeen > 0 ||
|
|
1407
1570
|
badFrames > 0;
|
|
1408
1571
|
if (!hasTerminal && !text && !cleanThinkingEarly && toolCalls.length === 0 && isSSEPayload) {
|
|
@@ -1433,14 +1596,10 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1433
1596
|
} catch (err: unknown) {
|
|
1434
1597
|
const raw = err instanceof Error ? err : new Error(String(err));
|
|
1435
1598
|
const isAbort =
|
|
1436
|
-
linked.signal.aborted ||
|
|
1437
|
-
eventStream.isCancelled() ||
|
|
1438
|
-
(raw as { name?: string }).name === "AbortError";
|
|
1599
|
+
linked.signal.aborted || eventStream.isCancelled() || (raw as { name?: string }).name === "AbortError";
|
|
1439
1600
|
if (isAbort) {
|
|
1440
1601
|
cancelBackground();
|
|
1441
|
-
eventStream.fail(
|
|
1442
|
-
Object.assign(new Error("Stream aborted"), { name: "AbortError" })
|
|
1443
|
-
);
|
|
1602
|
+
eventStream.fail(Object.assign(new Error("Stream aborted"), { name: "AbortError" }));
|
|
1444
1603
|
} else if (evictSessionId && usedChain && isNotFoundChainError(raw)) {
|
|
1445
1604
|
// Reactive self-heal, fail-clean variant (Rule 2): the `start`
|
|
1446
1605
|
// event is already delivered, so a transparent in-stream retry
|