agent-accelerator 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -4
- package/package.json +1 -1
- package/src/agent/context.ts +5 -2
- package/src/agent/loop.ts +10 -4
- package/src/index.ts +2 -0
- package/src/providers/google.ts +378 -120
- package/src/providers/openai-compat.ts +2 -0
- package/src/providers/openai.ts +2 -0
- package/src/providers/openrouter.ts +2 -0
- package/src/providers.ts +16 -0
- package/src/types/message.ts +15 -0
- package/src/types/model.ts +5 -1
- package/src/types/response.ts +5 -0
- package/src/utils/headers.ts +1 -1
- package/src/utils/retry.ts +2 -1
package/README.md
CHANGED
|
@@ -520,6 +520,24 @@ Model resolution never silently selects a default model: an empty model throws.
|
|
|
520
520
|
|
|
521
521
|
An unknown prefix with no catalog match still routes to a custom provider, allowing private model IDs and endpoints.
|
|
522
522
|
|
|
523
|
+
### Google long decode (Gemini 4)
|
|
524
|
+
|
|
525
|
+
Google caps one request at 65,536 output tokens. Set `maxOutputTokens` above
|
|
526
|
+
that on a `gemini-4-*` model and the SDK runs the request as a background
|
|
527
|
+
interaction automatically (no extra option): up to 1,048,576 tokens minus the
|
|
528
|
+
input, for up to 24 hours. `run()` polls until it finishes (2 s, then every
|
|
529
|
+
5 s); `stream()` reconnects after a dropped or silent connection
|
|
530
|
+
(`last_event_id`, 15 min of silence counts as dropped). Aborting cancels the
|
|
531
|
+
interaction on Google's side, so it stops billing. Each HTTP request is still
|
|
532
|
+
timed by `limits.requestTimeoutMs`, but the whole run is not. Any other Google
|
|
533
|
+
model with `maxOutputTokens` above 65,536 fails before the network with a
|
|
534
|
+
`ConfigError`. Use it for hard reasoning, not chat: you pay for every token of
|
|
535
|
+
every 64K hop.
|
|
536
|
+
|
|
537
|
+
A custom `Provider` can opt into the same treatment with the optional
|
|
538
|
+
`isLongRunning(model, options)` method: when it returns `true`, the loop skips
|
|
539
|
+
its whole-call, first-byte and idle timers and leaves timing to the adapter.
|
|
540
|
+
|
|
523
541
|
### Registry
|
|
524
542
|
|
|
525
543
|
* `resolveModel("google/x" | ModelSpec)` → `{ provider, modelId, modelSpec? }`. Use to inspect routing before execution.
|
|
@@ -1118,10 +1136,19 @@ Message {
|
|
|
1118
1136
|
role: "system" | "user" | "assistant" | "tool",
|
|
1119
1137
|
content: string | ContentPart[],
|
|
1120
1138
|
name?,
|
|
1121
|
-
thoughtSignature
|
|
1139
|
+
thoughtSignature?,
|
|
1140
|
+
providerSteps? // { provider, steps }: raw output steps of an assistant turn
|
|
1122
1141
|
}
|
|
1123
1142
|
```
|
|
1124
1143
|
|
|
1144
|
+
`providerSteps` keeps a provider's raw output for one assistant turn (today:
|
|
1145
|
+
Google's `steps`, including thought signatures). When Google needs the full
|
|
1146
|
+
history again (after a restart, an expired chain or a provider switch), those
|
|
1147
|
+
turns are resent exactly as received, which keeps reasoning continuity and
|
|
1148
|
+
the implicit-cache prefix intact. Other providers ignore it. Adapters return
|
|
1149
|
+
it on `ProviderGenerateResult` / per-turn stream `AgentResponse`; it is never
|
|
1150
|
+
part of `toJSON()`.
|
|
1151
|
+
|
|
1125
1152
|
### ProviderContext
|
|
1126
1153
|
|
|
1127
1154
|
```ts
|
|
@@ -1140,9 +1167,9 @@ Supported content parts include:
|
|
|
1140
1167
|
* `ThinkingPart { type:"thinking", thinking, thoughtSignature? }` — preserved reasoning.
|
|
1141
1168
|
* `ToolCallPart { type:"tool_call", id, name, arguments, rawArguments?, thoughtSignature? }`
|
|
1142
1169
|
* `ToolResultPart { type:"tool_result", id, name, result, isError? }`
|
|
1143
|
-
* `ImagePart { type:"image", image, mimeType? }`
|
|
1170
|
+
* `ImagePart { type:"image", image, mimeType?, resolution? }`
|
|
1144
1171
|
* `AudioPart { type:"audio", audio, mimeType? }`
|
|
1145
|
-
* `VideoPart { type:"video", video, mimeType? }`
|
|
1172
|
+
* `VideoPart { type:"video", video, mimeType?, resolution? }`
|
|
1146
1173
|
* `FilePart { type:"file", file, mimeType?, filename? }` — PDFs/documents.
|
|
1147
1174
|
|
|
1148
1175
|
`image`, `audio`, `video`, and `file` inputs accept:
|
|
@@ -1171,6 +1198,20 @@ Media normalization is handled automatically by providers (mapped to provider-na
|
|
|
1171
1198
|
Provider matrix: text + image + wav/mp3 audio + PDF work on all supported
|
|
1172
1199
|
providers. Video works on Gemini only.
|
|
1173
1200
|
|
|
1201
|
+
On Google, links Google fetches itself go as a `uri` without downloading:
|
|
1202
|
+
YouTube links, Files API links, `gs://` objects and public `https://` URLs of
|
|
1203
|
+
URL-supported types (PDF, CSV, text, JSON, PNG/JPEG/WebP/BMP, common video;
|
|
1204
|
+
max 15 MB). Audio URLs and `http://` links are downloaded and inlined. Inline
|
|
1205
|
+
types are checked before the network; Google reads only PDF and CSV
|
|
1206
|
+
documents. The supported lists are the `GOOGLE_*_MIME` constants at the top of
|
|
1207
|
+
`src/providers/google.ts`.
|
|
1208
|
+
|
|
1209
|
+
`resolution` (`MediaResolution`: `"low" | "medium" | "high" | "ultra_high"`)
|
|
1210
|
+
sets how much detail Google spends on one image or video, and so how many
|
|
1211
|
+
input tokens it costs (image: 280 / 560 / 1120 / 2240; video per frame:
|
|
1212
|
+
`low` and `medium` 70, `high` 280). Omit it for Google's default. Other
|
|
1213
|
+
providers ignore it with one `provider_warning`.
|
|
1214
|
+
|
|
1174
1215
|
Before any network call, the executor checks `image` / `audio` / `video` /
|
|
1175
1216
|
`file`-as-PDF parts against the catalog's `modalities.input` for that
|
|
1176
1217
|
provider/model and fails fast with a one-line error naming the gap
|
|
@@ -1285,7 +1326,7 @@ saveSessionFile(".session.json", agent, telemetry);
|
|
|
1285
1326
|
|
|
1286
1327
|
* `agent.exportSession(telemetry)` / `agent.importSession(saved)` round-trip messages, system prompt, model, thinking level, instructions, cache, worker model, and sub-agent traces without touching `AgentContext` internals.
|
|
1287
1328
|
* `SessionTelemetry` accumulates `input / output / cacheRead / cacheWrite / reasoning / cost`, with clamped `turnHitRate()` / `totalHitRate()`, a dual `formatBar(res)`, and `formatSessionBanner(saved, telemetry, path)` for startup. Cost follows the [TokenUsage](#response-usage-and-raw-data) rules.
|
|
1288
|
-
* Files are JSON with one line per top-level field and one line per turn (2-space indent), written atomically (temp + rename).
|
|
1329
|
+
* Files are JSON with one line per top-level field and one line per turn (2-space indent), written atomically (temp + rename). Google turns also store their raw `providerSteps`, so a resumed session replays them exactly.
|
|
1289
1330
|
* Binary media (bytes, data URLs, base64) is extracted to `media/` on save via `saveSessionDir("sessions", agent, telemetry)` → `sessions/<sessionId>/{session.json, media/*}`; remote URLs and local paths stay references. `loadSessionDir(dir)` resolves `media/…` refs back to absolute paths, and `findLatestSessionDir("sessions")` resumes the most recent session.
|
|
1290
1331
|
* Sub-agent traces persist in a dedicated `subagents` section keyed by TrackingID (`{ trackingId, name, sessionId, parentSessionId, status, task, turns, usage, steps, text }`). Files stay lean: redundant `rawArguments` and repeated step text are dropped on save (rebuilt/kept live in memory).
|
|
1291
1332
|
* With `persist: { dir }` or `{ file }`, the session file is rewritten after every step, so a crash loses at most the in-flight step.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-accelerator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "High-performance SDK for AI agents with a unified API across AI providers. Optimized for high cache hit rates, observability, and seamless agent orchestration. Built to power production-grade agentic systems and harnesses for teams at any scale.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sashvat Bharat",
|
package/src/agent/context.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Message, ContentPart } from "../types/message.ts";
|
|
1
|
+
import type { Message, ContentPart, ProviderSteps } from "../types/message.ts";
|
|
2
2
|
import type { ToolCallRecord, ToolResultRecord } from "../types/tool.ts";
|
|
3
3
|
import { safeStringify } from "../utils/serialization.ts";
|
|
4
4
|
import { clearSessionRouting } from "../providers.ts";
|
|
@@ -56,11 +56,13 @@ export class AgentContext {
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// Stores one model turn as an assistant message (text, thinking, tool calls, raw provider steps for replay); in: a provider turn from the loop, out: history.
|
|
59
60
|
addAssistantMessage(
|
|
60
61
|
text: string,
|
|
61
62
|
toolCalls?: ToolCallRecord[],
|
|
62
63
|
thinking?: string,
|
|
63
|
-
thoughtSignature?: string
|
|
64
|
+
thoughtSignature?: string,
|
|
65
|
+
providerSteps?: ProviderSteps
|
|
64
66
|
): void {
|
|
65
67
|
const parts: ContentPart[] = [];
|
|
66
68
|
const hasToolCalls = Boolean(toolCalls && toolCalls.length > 0);
|
|
@@ -110,6 +112,7 @@ export class AgentContext {
|
|
|
110
112
|
role: "assistant",
|
|
111
113
|
content: parts.length > 0 ? parts : text,
|
|
112
114
|
thoughtSignature,
|
|
115
|
+
...(providerSteps ? { providerSteps } : {}),
|
|
113
116
|
});
|
|
114
117
|
|
|
115
118
|
if (thoughtSignature) {
|
package/src/agent/loop.ts
CHANGED
|
@@ -666,6 +666,8 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
666
666
|
if (gTier === "paid" || gTier === "free") providerOptions.googleTier = gTier;
|
|
667
667
|
} catch {}
|
|
668
668
|
|
|
669
|
+
// A long-running call (e.g. Google long decode) times each HTTP request itself: no whole-call race.
|
|
670
|
+
const longRunning = provider.isLongRunning?.(modelId, providerOptions) === true;
|
|
669
671
|
const genResult = await generateWithTimeout(
|
|
670
672
|
() => provider.generate(
|
|
671
673
|
modelId,
|
|
@@ -675,7 +677,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
675
677
|
},
|
|
676
678
|
providerOptions
|
|
677
679
|
),
|
|
678
|
-
limits.requestTimeoutMs
|
|
680
|
+
longRunning ? Infinity : limits.requestTimeoutMs
|
|
679
681
|
);
|
|
680
682
|
|
|
681
683
|
finalResult = genResult;
|
|
@@ -710,7 +712,8 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
710
712
|
genResult.text,
|
|
711
713
|
genResult.toolCalls,
|
|
712
714
|
genResult.thinking,
|
|
713
|
-
genResult.thoughtSignature
|
|
715
|
+
genResult.thoughtSignature,
|
|
716
|
+
genResult.providerSteps
|
|
714
717
|
);
|
|
715
718
|
|
|
716
719
|
// Thinking-trace telemetry (observability-only, full text, no limit):
|
|
@@ -1053,6 +1056,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1053
1056
|
if (gTier === "paid" || gTier === "free") providerOptions.googleTier = gTier;
|
|
1054
1057
|
} catch {}
|
|
1055
1058
|
|
|
1059
|
+
const longRunning = provider.isLongRunning?.(modelId, providerOptions) === true;
|
|
1056
1060
|
const innerStream = provider.stream(
|
|
1057
1061
|
modelId,
|
|
1058
1062
|
{
|
|
@@ -1071,7 +1075,8 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1071
1075
|
let first = true;
|
|
1072
1076
|
let doneIter = false;
|
|
1073
1077
|
while (!doneIter) {
|
|
1074
|
-
|
|
1078
|
+
// Long-running streams pause for minutes between chunks; the adapter guards stalls and reconnects.
|
|
1079
|
+
const waitMs = longRunning ? Infinity : first ? streamLimits.firstByteTimeoutMs : streamLimits.streamIdleTimeoutMs;
|
|
1075
1080
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1076
1081
|
const timeoutP =
|
|
1077
1082
|
Number.isFinite(waitMs) && waitMs !== Infinity && waitMs > 0
|
|
@@ -1147,7 +1152,8 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1147
1152
|
turnResponse.text,
|
|
1148
1153
|
turnResponse.toolCalls,
|
|
1149
1154
|
turnResponse.thinking,
|
|
1150
|
-
turnResponse.thoughtSignature
|
|
1155
|
+
turnResponse.thoughtSignature,
|
|
1156
|
+
turnResponse.providerSteps
|
|
1151
1157
|
);
|
|
1152
1158
|
|
|
1153
1159
|
// Thinking-trace telemetry (observability-only, full text, no limit):
|
package/src/index.ts
CHANGED
package/src/providers/google.ts
CHANGED
|
@@ -29,18 +29,19 @@ import type {
|
|
|
29
29
|
ProviderGenerateResult,
|
|
30
30
|
ProviderRawData,
|
|
31
31
|
} from "../types/model.ts";
|
|
32
|
-
import type { ProviderContext, Message, ContentPart } from "../types/message.ts";
|
|
32
|
+
import type { ProviderContext, Message, ContentPart, ProviderSteps, ToolResultPart, MediaResolution } from "../types/message.ts";
|
|
33
33
|
import type { StandardToolDeclaration, ToolCallRecord } from "../types/tool.ts";
|
|
34
34
|
import type { TokenUsage } from "../types/core.ts";
|
|
35
35
|
import { AssistantMessageEventStream } from "../streaming/event-stream.ts";
|
|
36
36
|
import { SSEParser } from "../streaming/sse-parser.ts";
|
|
37
37
|
import { AgentResponse } from "../types/response.ts";
|
|
38
38
|
import { getApiKey, getEnv } from "../utils/env.ts";
|
|
39
|
-
import { normalizeMediaInput } from "../utils/media.ts";
|
|
39
|
+
import { normalizeMediaInput, inferMimeType } from "../utils/media.ts";
|
|
40
|
+
import { ConfigError, ValidationError, isAbortError } from "../types/errors.ts";
|
|
40
41
|
import { safeStringify } from "../utils/serialization.ts";
|
|
41
42
|
import { stripSchemaForGoogle } from "../tools/schema.ts";
|
|
42
43
|
import { toConciseProviderError, assertModalitiesSupported } from "../utils/errors.ts";
|
|
43
|
-
import { withRetries } from "../utils/retry.ts";
|
|
44
|
+
import { withRetries, sleep } from "../utils/retry.ts";
|
|
44
45
|
import { setHeaderCaseInsensitive, shouldEchoSignature } from "../utils/headers.ts";
|
|
45
46
|
import {
|
|
46
47
|
newToolCallId,
|
|
@@ -77,16 +78,102 @@ import {
|
|
|
77
78
|
emitProviderWarning,
|
|
78
79
|
} from "../providers.ts";
|
|
79
80
|
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Supported inputs (api-ref `interactions-api.md` enums, `docs/file-input-methods.md`)
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/** Image MIME types Google accepts inline. */
|
|
86
|
+
export const GOOGLE_IMAGE_MIME = ["image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", "image/gif", "image/bmp", "image/tiff"] as const;
|
|
87
|
+
/** Audio MIME types Google accepts inline (audio is never fetched by URL). */
|
|
88
|
+
export const GOOGLE_AUDIO_MIME = ["audio/wav", "audio/mp3", "audio/aiff", "audio/aac", "audio/ogg", "audio/flac", "audio/mpeg", "audio/m4a", "audio/l16", "audio/opus", "audio/alaw", "audio/mulaw", "audio/webm"] as const;
|
|
89
|
+
/** Video MIME types Google accepts inline. */
|
|
90
|
+
export const GOOGLE_VIDEO_MIME = ["video/mp4", "video/mpeg", "video/mpg", "video/mov", "video/avi", "video/x-flv", "video/webm", "video/wmv", "video/3gpp"] as const;
|
|
91
|
+
/** Document MIME types Google accepts (the only two). */
|
|
92
|
+
export const GOOGLE_DOCUMENT_MIME = ["application/pdf", "text/csv"] as const;
|
|
93
|
+
/** MIME types Google fetches itself from a public `https://` URL. */
|
|
94
|
+
export const GOOGLE_URL_MIME = [
|
|
95
|
+
"text/html", "text/css", "text/plain", "text/xml", "text/csv", "text/rtf", "text/javascript",
|
|
96
|
+
"application/json", "application/pdf",
|
|
97
|
+
"image/bmp", "image/jpeg", "image/png", "image/webp",
|
|
98
|
+
"video/mp4", "video/mpeg", "video/quicktime", "video/avi", "video/x-flv", "video/mpg", "video/webm", "video/wmv", "video/3gpp",
|
|
99
|
+
] as const;
|
|
100
|
+
/** Common MIME spellings mapped to Google's inline names. */
|
|
101
|
+
export const GOOGLE_INLINE_MIME_ALIASES: Readonly<Record<string, string>> = {
|
|
102
|
+
"video/quicktime": "video/mov",
|
|
103
|
+
"audio/x-wav": "audio/wav",
|
|
104
|
+
"audio/wave": "audio/wav",
|
|
105
|
+
"image/jpg": "image/jpeg",
|
|
106
|
+
};
|
|
107
|
+
// Documented size limits. Not enforced yet: oversized inputs get Google's own error.
|
|
108
|
+
/** Max inline payload per request (bytes). */
|
|
109
|
+
export const GOOGLE_MAX_INLINE_REQUEST_BYTES = 100 * 1024 * 1024;
|
|
110
|
+
/** Max inline PDF size (bytes). */
|
|
111
|
+
export const GOOGLE_MAX_INLINE_PDF_BYTES = 50 * 1024 * 1024;
|
|
112
|
+
/** Max size of one file Google fetches by URL (bytes). */
|
|
113
|
+
export const GOOGLE_MAX_URL_FILE_BYTES = 15 * 1024 * 1024;
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Long decode (`docs/long-decode.md`, `docs/background-execution.md`)
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/** Output tokens one non-background request may produce; above it Gemini 4 runs as a background interaction (long decode). */
|
|
120
|
+
export const GOOGLE_MAX_SYNC_OUTPUT_TOKENS = 65_536;
|
|
121
|
+
// A background stream pauses between ~5K-token chunks; only this much silence counts as a dropped connection (docs: 15-20 min).
|
|
122
|
+
const BACKGROUND_STREAM_STALL_MS = 15 * 60_000;
|
|
123
|
+
// Reconnects in a row that bring no new event before a background stream gives up.
|
|
124
|
+
const BACKGROUND_MAX_RECONNECTS = 5;
|
|
125
|
+
|
|
126
|
+
// True when a request needs Gemini 4 long decode (background); any other model above the limit fails before the network; in: model id + maxOutputTokens, out: buildBody, isLongRunning.
|
|
127
|
+
function needsLongDecode(modelId: string, maxOutputTokens: number | undefined): boolean {
|
|
128
|
+
if (!maxOutputTokens || maxOutputTokens <= GOOGLE_MAX_SYNC_OUTPUT_TOKENS) return false;
|
|
129
|
+
if (/^gemini-4/i.test(modelId)) return true;
|
|
130
|
+
throw new ConfigError(
|
|
131
|
+
`[Agent Accelerator] [google/${modelId}] maxOutputTokens ${maxOutputTokens} is above Google's ${GOOGLE_MAX_SYNC_OUTPUT_TOKENS} limit; only Gemini 4 models can write more (long decode). Lower maxOutputTokens or use a gemini-4 model.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// True while a background interaction is still working; in: Interaction.status, out: polling and stream reconnects.
|
|
136
|
+
function isRunning(status: string | undefined): boolean {
|
|
137
|
+
return status === "queued" || status === "in_progress";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// The two reader calls the stream loop uses (Bun ships two incompatible reader typings, so only these are typed).
|
|
141
|
+
type StreamReader = { read(): Promise<{ done: boolean; value?: Uint8Array }>; cancel(): Promise<void> };
|
|
142
|
+
|
|
143
|
+
// Reads the next chunk, or cancels the reader and rejects after `ms` of silence; in: a background stream reader, out: the stream read loop.
|
|
144
|
+
async function readWithin(reader: StreamReader, ms: number) {
|
|
145
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
146
|
+
const stalled = new Promise<never>((_, reject) => {
|
|
147
|
+
timer = setTimeout(() => {
|
|
148
|
+
void reader.cancel().catch(() => {});
|
|
149
|
+
reject(new Error("Google background stream stalled"));
|
|
150
|
+
}, ms);
|
|
151
|
+
});
|
|
152
|
+
try {
|
|
153
|
+
return await Promise.race([reader.read(), stalled]);
|
|
154
|
+
} finally {
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
type MediaBlockType = "image" | "audio" | "video" | "document";
|
|
160
|
+
|
|
161
|
+
const INLINE_MIME: Record<MediaBlockType, readonly string[]> = {
|
|
162
|
+
image: GOOGLE_IMAGE_MIME,
|
|
163
|
+
audio: GOOGLE_AUDIO_MIME,
|
|
164
|
+
video: GOOGLE_VIDEO_MIME,
|
|
165
|
+
document: GOOGLE_DOCUMENT_MIME,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const FILES_API_PREFIX = "https://generativelanguage.googleapis.com/v1beta/files/";
|
|
169
|
+
const YOUTUBE_URL = /^https:\/\/(www\.|m\.)?(youtube\.com|youtu\.be)\//i;
|
|
170
|
+
|
|
80
171
|
// ---------------------------------------------------------------------------
|
|
81
172
|
// Interactions wire shapes (subset used by this adapter)
|
|
82
173
|
// ---------------------------------------------------------------------------
|
|
83
174
|
|
|
84
|
-
type
|
|
85
|
-
|
|
86
|
-
| { type: "image"; mime_type: string; data?: string; uri?: string }
|
|
87
|
-
| { type: "audio"; mime_type: string; data?: string; uri?: string }
|
|
88
|
-
| { type: "video"; mime_type: string; data?: string; uri?: string }
|
|
89
|
-
| { type: "document"; mime_type: string; data?: string; uri?: string };
|
|
175
|
+
type MediaBlock = { type: MediaBlockType; mime_type?: string; data?: string; uri?: string; resolution?: MediaResolution };
|
|
176
|
+
type InteractionContentBlock = { type: "text"; text: string } | MediaBlock;
|
|
90
177
|
|
|
91
178
|
type InteractionInputStep =
|
|
92
179
|
| { type: "user_input"; content: string | InteractionContentBlock[] }
|
|
@@ -97,6 +184,7 @@ type InteractionInputStep =
|
|
|
97
184
|
name: string;
|
|
98
185
|
call_id: string;
|
|
99
186
|
result: InteractionContentBlock[];
|
|
187
|
+
is_error?: boolean;
|
|
100
188
|
}
|
|
101
189
|
| { type: string; [k: string]: unknown };
|
|
102
190
|
|
|
@@ -111,6 +199,7 @@ interface InteractionRequestBody {
|
|
|
111
199
|
store?: boolean;
|
|
112
200
|
previous_interaction_id?: string;
|
|
113
201
|
stream?: boolean;
|
|
202
|
+
background?: boolean;
|
|
114
203
|
}
|
|
115
204
|
|
|
116
205
|
interface InteractionStep {
|
|
@@ -147,6 +236,14 @@ interface InteractionObject {
|
|
|
147
236
|
[k: string]: unknown;
|
|
148
237
|
}
|
|
149
238
|
|
|
239
|
+
/** One parsed Interactions HTTP reply (create POST or poll GET). */
|
|
240
|
+
interface InteractionReply {
|
|
241
|
+
status: number;
|
|
242
|
+
statusText: string;
|
|
243
|
+
headers: Record<string, string>;
|
|
244
|
+
interaction: InteractionObject;
|
|
245
|
+
}
|
|
246
|
+
|
|
150
247
|
const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
151
248
|
|
|
152
249
|
/** Per-session interaction chaining (provider-generated ids only). */
|
|
@@ -321,48 +418,102 @@ function toGoogleToolChoice(
|
|
|
321
418
|
return { allowed_tools: { mode: norm.mode } };
|
|
322
419
|
}
|
|
323
420
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
421
|
+
type MediaValue = string | Uint8Array | ArrayBuffer;
|
|
422
|
+
|
|
423
|
+
// Lower-cases a MIME type and drops parameters (`text/csv; charset=utf-8` -> `text/csv`); in: part or response MIME, out: list checks.
|
|
424
|
+
function bareMime(mime: string | undefined): string | undefined {
|
|
425
|
+
return mime ? mime.split(";")[0]!.trim().toLowerCase() || undefined : undefined;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Returns the `uri` block for a link Google fetches itself (YouTube, Files API, `gs://`, https URL of a URL-supported type), else undefined; in: a media part's string value, out: contentPartsToBlocks.
|
|
429
|
+
function uriBlock(type: MediaBlockType, value: string, mimeType?: string): MediaBlock | undefined {
|
|
430
|
+
const explicit = bareMime(mimeType);
|
|
431
|
+
const withMime = (mime?: string): MediaBlock => ({ type, uri: value, ...(mime ? { mime_type: mime } : {}) });
|
|
432
|
+
if (type === "video" && YOUTUBE_URL.test(value)) return withMime(explicit);
|
|
433
|
+
const mime = explicit ?? bareMime(inferMimeType(value, ""));
|
|
434
|
+
if (value.startsWith(FILES_API_PREFIX) || value.startsWith("gs://")) return withMime(mime);
|
|
435
|
+
return value.startsWith("https://") && mime && (GOOGLE_URL_MIME as readonly string[]).includes(mime) ? withMime(mime) : undefined;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// 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.
|
|
439
|
+
async function inlineBlock(type: MediaBlockType, value: MediaValue, mimeType?: string): Promise<MediaBlock> {
|
|
440
|
+
const norm = await normalizeMediaInput(value, mimeType);
|
|
441
|
+
const raw = bareMime(norm.mimeType) ?? "";
|
|
442
|
+
const mime = GOOGLE_INLINE_MIME_ALIASES[raw] ?? raw;
|
|
443
|
+
if (!INLINE_MIME[type].includes(mime)) {
|
|
444
|
+
const what = type === "document" ? "Google reads only PDF and CSV documents" : `Google does not accept ${type} type "${mime}"`;
|
|
445
|
+
throw new ValidationError(
|
|
446
|
+
`[Agent Accelerator] ${what} (got "${mime}"; supported: ${INLINE_MIME[type].join(", ")}). Convert the file or pass the correct mimeType.`
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
return { type, mime_type: mime, data: norm.base64Data };
|
|
330
450
|
}
|
|
331
451
|
|
|
452
|
+
// Maps canonical text and media parts to Interactions content blocks (one builder for chained and full-history input); in: a message's parts, out: request `input`.
|
|
332
453
|
async function contentPartsToBlocks(parts: ContentPart[]): Promise<InteractionContentBlock[]> {
|
|
333
454
|
const blocks: InteractionContentBlock[] = [];
|
|
334
455
|
for (const part of parts) {
|
|
335
|
-
if (part.type === "text"
|
|
336
|
-
blocks.push({ type: "text", text: part.text });
|
|
337
|
-
|
|
338
|
-
part.type === "image" ||
|
|
339
|
-
part.type === "audio" ||
|
|
340
|
-
part.type === "video" ||
|
|
341
|
-
part.type === "file"
|
|
342
|
-
) {
|
|
343
|
-
const raw = (part as { image?: unknown; audio?: unknown; video?: unknown; file?: unknown }).image ??
|
|
344
|
-
(part as { audio?: unknown }).audio ??
|
|
345
|
-
(part as { video?: unknown }).video ??
|
|
346
|
-
(part as { file?: unknown }).file;
|
|
347
|
-
const norm = await normalizeMediaInput(
|
|
348
|
-
raw as string | Uint8Array | ArrayBuffer,
|
|
349
|
-
(part as { mimeType?: string }).mimeType
|
|
350
|
-
);
|
|
351
|
-
blocks.push({
|
|
352
|
-
type: mediaBlockType(part.type),
|
|
353
|
-
mime_type: norm.mimeType,
|
|
354
|
-
data: norm.base64Data,
|
|
355
|
-
} as InteractionContentBlock);
|
|
456
|
+
if (part.type === "text") {
|
|
457
|
+
if (part.text) blocks.push({ type: "text", text: part.text });
|
|
458
|
+
continue;
|
|
356
459
|
}
|
|
460
|
+
if (part.type !== "image" && part.type !== "audio" && part.type !== "video" && part.type !== "file") continue;
|
|
461
|
+
const type: MediaBlockType = part.type === "file" ? "document" : part.type;
|
|
462
|
+
const value = (part as unknown as Record<string, MediaValue>)[part.type]!;
|
|
463
|
+
const uri = typeof value === "string" ? uriBlock(type, value, part.mimeType) : undefined;
|
|
464
|
+
const block = uri ?? (await inlineBlock(type, value, part.mimeType));
|
|
465
|
+
// Per-item detail level (image and video blocks only, api-ref `MediaResolution`).
|
|
466
|
+
const resolution = part.type === "image" || part.type === "video" ? part.resolution : undefined;
|
|
467
|
+
blocks.push(resolution ? { ...block, resolution } : block);
|
|
357
468
|
}
|
|
358
469
|
return blocks;
|
|
359
470
|
}
|
|
360
471
|
|
|
472
|
+
// 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.
|
|
473
|
+
function replaySteps(steps: unknown[] | undefined): ProviderSteps | undefined {
|
|
474
|
+
const kept = (steps ?? [])
|
|
475
|
+
.filter((s) => {
|
|
476
|
+
const type = (s as { type?: unknown } | null)?.type;
|
|
477
|
+
return typeof type === "string" && type !== "user_input" && type !== "function_result";
|
|
478
|
+
})
|
|
479
|
+
.map((s) => {
|
|
480
|
+
const { signature, ...rest } = s as Record<string, unknown>;
|
|
481
|
+
return signature === "" ? rest : s;
|
|
482
|
+
});
|
|
483
|
+
return kept.length > 0 ? { provider: "google", steps: kept } : undefined;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Appends streamed text to the last text block of a step's `content`/`summary`, or starts one; in: step.delta, out: the assembled stream step.
|
|
487
|
+
function appendBlockText(step: Record<string, unknown> | undefined, key: "content" | "summary", text: string): void {
|
|
488
|
+
if (!step) return;
|
|
489
|
+
const blocks = (Array.isArray(step[key]) ? step[key] : (step[key] = [])) as Array<{ type?: string; text?: string }>;
|
|
490
|
+
const last = blocks[blocks.length - 1];
|
|
491
|
+
if (last?.type === "text" && typeof last.text === "string") last.text += text;
|
|
492
|
+
else blocks.push({ type: "text", text });
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Joins the `text` of content/summary blocks; in: a `step.start` step's `content` or `summary`, out: stream text/thinking.
|
|
496
|
+
function blockText(blocks: unknown): string {
|
|
497
|
+
if (!Array.isArray(blocks)) return "";
|
|
498
|
+
return blocks.map((b) => (typeof b?.text === "string" ? b.text : "")).join("");
|
|
499
|
+
}
|
|
500
|
+
|
|
361
501
|
function resultToText(result: unknown): string {
|
|
362
502
|
if (typeof result === "string") return result;
|
|
363
503
|
return safeStringify(result);
|
|
364
504
|
}
|
|
365
505
|
|
|
506
|
+
// 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`.
|
|
507
|
+
function functionResultStep(part: ToolResultPart): InteractionInputStep {
|
|
508
|
+
return {
|
|
509
|
+
type: "function_result",
|
|
510
|
+
name: part.name,
|
|
511
|
+
call_id: part.id,
|
|
512
|
+
result: [{ type: "text", text: resultToText(part.result) }],
|
|
513
|
+
...(part.isError ? { is_error: true } : {}),
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
366
517
|
/**
|
|
367
518
|
* Latest-turn input for stateful chaining (the server already holds history):
|
|
368
519
|
* everything AFTER the last assistant message, in order. That is normally one
|
|
@@ -400,12 +551,7 @@ async function latestTurnInput(context: ProviderContext): Promise<string | Inter
|
|
|
400
551
|
if (!Array.isArray(m.content)) continue;
|
|
401
552
|
for (const part of m.content) {
|
|
402
553
|
if (part.type === "tool_result") {
|
|
403
|
-
steps.push(
|
|
404
|
-
type: "function_result",
|
|
405
|
-
name: part.name,
|
|
406
|
-
call_id: part.id,
|
|
407
|
-
result: [{ type: "text", text: resultToText(part.result) }],
|
|
408
|
-
});
|
|
554
|
+
steps.push(functionResultStep(part));
|
|
409
555
|
}
|
|
410
556
|
}
|
|
411
557
|
} else if (m.role === "user") {
|
|
@@ -420,7 +566,7 @@ async function latestTurnInput(context: ProviderContext): Promise<string | Inter
|
|
|
420
566
|
return steps;
|
|
421
567
|
}
|
|
422
568
|
|
|
423
|
-
|
|
569
|
+
// Rebuilds the whole conversation as Interactions steps; Google turns replay their stored raw steps verbatim, others are reconstructed; in: AgentContext messages, out: request `input` on reseed/switch turns.
|
|
424
570
|
async function fullHistorySteps(context: ProviderContext): Promise<InteractionInputStep[]> {
|
|
425
571
|
const steps: InteractionInputStep[] = [];
|
|
426
572
|
for (const m of context.messages) {
|
|
@@ -433,6 +579,11 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
433
579
|
if (blocks.length > 0) steps.push({ type: "user_input", content: blocks });
|
|
434
580
|
}
|
|
435
581
|
} else if (m.role === "assistant") {
|
|
582
|
+
// Exact replay: same bytes as the server returned (thought signatures included), best for implicit caching.
|
|
583
|
+
if (m.providerSteps?.provider === "google") {
|
|
584
|
+
steps.push(...(m.providerSteps.steps as InteractionInputStep[]));
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
436
587
|
if (typeof m.content === "string") {
|
|
437
588
|
if (m.content) {
|
|
438
589
|
steps.push({
|
|
@@ -443,6 +594,7 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
443
594
|
continue;
|
|
444
595
|
}
|
|
445
596
|
const texts: string[] = [];
|
|
597
|
+
const turnStart = steps.length;
|
|
446
598
|
for (const part of m.content) {
|
|
447
599
|
if (part.type === "thinking" && part.thinking) {
|
|
448
600
|
// Q-18: preserve namespaced signatures only — foreign-provider
|
|
@@ -475,7 +627,7 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
475
627
|
type: "model_output",
|
|
476
628
|
content: texts.map((t) => ({ type: "text", text: t })),
|
|
477
629
|
} as InteractionInputStep);
|
|
478
|
-
} else if (m.thoughtSignature && !steps.some((s) => s.type === "thought")) {
|
|
630
|
+
} else if (m.thoughtSignature && !steps.slice(turnStart).some((s) => s.type === "thought")) {
|
|
479
631
|
// Preserve a signature-only thought so stateless chaining keeps
|
|
480
632
|
// reasoning continuity even when no summary text was retained.
|
|
481
633
|
// Q-18: only for google/untagged signatures.
|
|
@@ -488,12 +640,7 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
488
640
|
if (!Array.isArray(m.content)) continue;
|
|
489
641
|
for (const part of m.content) {
|
|
490
642
|
if (part.type === "tool_result") {
|
|
491
|
-
steps.push(
|
|
492
|
-
type: "function_result",
|
|
493
|
-
name: part.name,
|
|
494
|
-
call_id: part.id,
|
|
495
|
-
result: [{ type: "text", text: resultToText(part.result) }],
|
|
496
|
-
});
|
|
643
|
+
steps.push(functionResultStep(part));
|
|
497
644
|
}
|
|
498
645
|
}
|
|
499
646
|
}
|
|
@@ -597,6 +744,7 @@ function parseInteraction(
|
|
|
597
744
|
provider: "google",
|
|
598
745
|
raw,
|
|
599
746
|
durationMs,
|
|
747
|
+
providerSteps: replaySteps(interaction.steps),
|
|
600
748
|
};
|
|
601
749
|
}
|
|
602
750
|
|
|
@@ -661,6 +809,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
661
809
|
model: modelId,
|
|
662
810
|
input: "",
|
|
663
811
|
...(stream ? { stream: true } : {}),
|
|
812
|
+
...(needsLongDecode(modelId, options?.maxOutputTokens) ? { background: true } : {}),
|
|
664
813
|
};
|
|
665
814
|
// Structured outputs: native `response_format` when supported, else
|
|
666
815
|
// emulated via system instruction (catalog `structured_output: false`
|
|
@@ -717,7 +866,8 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
717
866
|
// provider's turns, so send the full canonical history explicitly and
|
|
718
867
|
// statelessly, then resume chaining fresh on the next turn.
|
|
719
868
|
body.input = await fullHistorySteps(context);
|
|
720
|
-
|
|
869
|
+
// Background runs need storage (`store: false` is a 400); this turn then simply starts a fresh chain.
|
|
870
|
+
if (!body.background) body.store = false;
|
|
721
871
|
} else if (continuing) {
|
|
722
872
|
body.input = await latestTurnInput(context);
|
|
723
873
|
body.previous_interaction_id = chain!.interactionId;
|
|
@@ -735,7 +885,8 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
735
885
|
}
|
|
736
886
|
|
|
737
887
|
private requestInit(
|
|
738
|
-
|
|
888
|
+
method: "POST" | "GET",
|
|
889
|
+
body: InteractionRequestBody | undefined,
|
|
739
890
|
options: ProviderRequestOptions | undefined,
|
|
740
891
|
apiKey: string,
|
|
741
892
|
signal?: AbortSignal
|
|
@@ -746,19 +897,20 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
746
897
|
// `X-Goog-Api-Key` never duplicates the canonical header. Proxy flows
|
|
747
898
|
// (`apiKey === "unused"`) preserve custom auth instead of overwriting.
|
|
748
899
|
if (apiKey !== "unused") setHeaderCaseInsensitive(headers, "x-goog-api-key", apiKey);
|
|
749
|
-
return { method
|
|
900
|
+
return { method, headers, ...(body ? { body: JSON.stringify(body) } : {}), signal };
|
|
750
901
|
}
|
|
751
902
|
|
|
752
903
|
private async doFetch(
|
|
753
904
|
url: string,
|
|
754
|
-
|
|
905
|
+
method: "POST" | "GET",
|
|
906
|
+
body: InteractionRequestBody | undefined,
|
|
755
907
|
options: ProviderRequestOptions | undefined,
|
|
756
908
|
apiKey: string,
|
|
757
909
|
signal?: AbortSignal
|
|
758
910
|
): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
|
|
759
911
|
const { signal: effective, cleanup } = combinedSignal(signal, timeoutFor(options as { timeoutMs?: number } | undefined));
|
|
760
912
|
try {
|
|
761
|
-
const res = await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, effective));
|
|
913
|
+
const res = await (options?.fetch ?? fetch)(url, this.requestInit(method, body, options, apiKey, effective));
|
|
762
914
|
const text = await res.text();
|
|
763
915
|
const headers: Record<string, string> = {};
|
|
764
916
|
res.headers.forEach((v, k) => {
|
|
@@ -799,6 +951,45 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
799
951
|
throw toConciseProviderError(err, "google", modelId);
|
|
800
952
|
}
|
|
801
953
|
|
|
954
|
+
/** Poll delays (ms) for background interactions: before the first poll, then between polls. */
|
|
955
|
+
pollDelaysMs: [number, number] = [2_000, 5_000];
|
|
956
|
+
|
|
957
|
+
/** True for Gemini 4 long decode (`maxOutputTokens` above 65,536): the request runs in the background for up to 24 h. */
|
|
958
|
+
isLongRunning(model: string | ModelSpec, options?: ProviderRequestOptions): boolean {
|
|
959
|
+
return needsLongDecode(cleanModelId(model), options?.maxOutputTokens);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// One Interactions HTTP call parsed into its interaction (errors mapped concisely); in: create POST or poll GET, out: generate.
|
|
963
|
+
private async fetchInteraction(
|
|
964
|
+
url: string,
|
|
965
|
+
method: "POST" | "GET",
|
|
966
|
+
body: InteractionRequestBody | undefined,
|
|
967
|
+
options: ProviderRequestOptions | undefined,
|
|
968
|
+
apiKey: string,
|
|
969
|
+
modelId: string
|
|
970
|
+
): Promise<InteractionReply> {
|
|
971
|
+
const res = await this.doFetch(url, method, body, options, apiKey, options?.signal);
|
|
972
|
+
this.throwIfError(res.status, res.statusText, url, res.text, modelId, res.headers);
|
|
973
|
+
try {
|
|
974
|
+
return { status: res.status, statusText: res.statusText, headers: res.headers, interaction: JSON.parse(res.text) as InteractionObject };
|
|
975
|
+
} catch {
|
|
976
|
+
throw toConciseProviderError(
|
|
977
|
+
Object.assign(new Error("Invalid JSON response from Google Interactions API"), {
|
|
978
|
+
statusCode: res.status,
|
|
979
|
+
responseBody: res.text.slice(0, 500),
|
|
980
|
+
url,
|
|
981
|
+
}),
|
|
982
|
+
"google",
|
|
983
|
+
modelId
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Fire-and-forget cancel so an aborted background run stops billing; in: an aborted generate/stream, out: POST {id}/cancel.
|
|
989
|
+
private cancelInteraction(baseUrl: string, id: string, options: ProviderRequestOptions | undefined, apiKey: string): void {
|
|
990
|
+
void this.doFetch(`${baseUrl}/interactions/${id}/cancel`, "POST", undefined, options, apiKey).catch(() => {});
|
|
991
|
+
}
|
|
992
|
+
|
|
802
993
|
async generate(
|
|
803
994
|
model: string | ModelSpec,
|
|
804
995
|
context: ProviderContext,
|
|
@@ -835,24 +1026,33 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
835
1026
|
body: b,
|
|
836
1027
|
});
|
|
837
1028
|
let rawRequest = makeRawRequest(body);
|
|
1029
|
+
const retryOptions = {
|
|
1030
|
+
...retryOptionsFrom(options),
|
|
1031
|
+
signal: options?.signal,
|
|
1032
|
+
label: { providerId: "google", modelId: clean },
|
|
1033
|
+
};
|
|
838
1034
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1035
|
+
// Polls a background interaction until it leaves queued/in_progress; an abort also cancels it server-side.
|
|
1036
|
+
const waitForCompletion = async (reply: InteractionReply): Promise<InteractionReply> => {
|
|
1037
|
+
const id = reply.interaction.id;
|
|
1038
|
+
if (!id) return reply;
|
|
843
1039
|
try {
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
"google",
|
|
853
|
-
clean
|
|
854
|
-
);
|
|
1040
|
+
for (let poll = 0; isRunning(reply.interaction.status); poll++) {
|
|
1041
|
+
await sleep(this.pollDelaysMs[poll === 0 ? 0 : 1], options?.signal);
|
|
1042
|
+
reply = await withRetries(() => this.fetchInteraction(`${url}/${id}`, "GET", undefined, options, apiKey, clean), retryOptions);
|
|
1043
|
+
}
|
|
1044
|
+
return reply;
|
|
1045
|
+
} catch (err) {
|
|
1046
|
+
if (options?.signal?.aborted || isAbortError(err)) this.cancelInteraction(baseUrl, id, options, apiKey);
|
|
1047
|
+
throw err;
|
|
855
1048
|
}
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
// Create (retried as a whole), wait if it runs in the background, then record the chain and map the result.
|
|
1052
|
+
const run = async (): Promise<ProviderGenerateResult> => {
|
|
1053
|
+
let reply = await withRetries(() => this.fetchInteraction(url, "POST", body, options, apiKey, clean), retryOptions);
|
|
1054
|
+
if (body.background) reply = await waitForCompletion(reply);
|
|
1055
|
+
const { interaction } = reply;
|
|
856
1056
|
if (interaction.id && sessionId && body.store !== false) {
|
|
857
1057
|
setChain(sessionId, { interactionId: interaction.id, model: clean, at: Date.now() });
|
|
858
1058
|
} else if (body.store === false && sessionId) {
|
|
@@ -864,19 +1064,14 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
864
1064
|
interaction,
|
|
865
1065
|
clean,
|
|
866
1066
|
Date.now() - startTime,
|
|
867
|
-
{ request: rawRequest, response: { status:
|
|
1067
|
+
{ request: rawRequest, response: { status: reply.status, statusText: reply.statusText, headers: reply.headers, body: interaction } }
|
|
868
1068
|
);
|
|
869
1069
|
checkTurnTokens(sessionId, parsed.usage);
|
|
870
1070
|
return parsed;
|
|
871
1071
|
};
|
|
872
1072
|
|
|
873
|
-
const retryOptions = {
|
|
874
|
-
...retryOptionsFrom(options),
|
|
875
|
-
signal: options?.signal,
|
|
876
|
-
label: { providerId: "google", modelId: clean },
|
|
877
|
-
};
|
|
878
1073
|
try {
|
|
879
|
-
return await
|
|
1074
|
+
return await run();
|
|
880
1075
|
} catch (err) {
|
|
881
1076
|
if (err instanceof Error && (err as { name?: string }).name === "AbortError") throw err;
|
|
882
1077
|
// Reactive self-heal (Rule 2): a chained pointer the server no longer
|
|
@@ -887,7 +1082,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
887
1082
|
clearInteractionChains(sessionId);
|
|
888
1083
|
body = await this.buildBody(clean, context, options, sessionId, tools, false);
|
|
889
1084
|
rawRequest = makeRawRequest(body);
|
|
890
|
-
return await
|
|
1085
|
+
return await run();
|
|
891
1086
|
}
|
|
892
1087
|
throw err;
|
|
893
1088
|
}
|
|
@@ -925,6 +1120,8 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
925
1120
|
// visible in catch).
|
|
926
1121
|
let evictSessionId: string | undefined;
|
|
927
1122
|
let usedChain = false;
|
|
1123
|
+
// Set once a background interaction id is known: an abort then cancels it server-side.
|
|
1124
|
+
let cancelBackground = (): void => {};
|
|
928
1125
|
try {
|
|
929
1126
|
// Hard generation floor (mirrors generate): no network on legacy ids.
|
|
930
1127
|
assertSupportedGeneration(clean);
|
|
@@ -958,30 +1155,32 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
958
1155
|
};
|
|
959
1156
|
|
|
960
1157
|
const streamTimeoutMs = timeoutFor(options as { timeoutMs?: number } | undefined);
|
|
961
|
-
//
|
|
962
|
-
//
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1158
|
+
// Opens one SSE connection (the create POST, or a background resume GET). Transient failures
|
|
1159
|
+
// (429/5xx/network) are retried before the first byte; a live stream is never retried.
|
|
1160
|
+
const open = (target: string, method: "POST" | "GET"): Promise<Response> =>
|
|
1161
|
+
openStreamResponse({
|
|
1162
|
+
fetchOnce: async () => {
|
|
1163
|
+
const timer =
|
|
1164
|
+
streamTimeoutMs && streamTimeoutMs > 0
|
|
1165
|
+
? setTimeout(() => {
|
|
1166
|
+
try {
|
|
1167
|
+
linked.abort();
|
|
1168
|
+
} catch {}
|
|
1169
|
+
}, streamTimeoutMs)
|
|
1170
|
+
: undefined;
|
|
1171
|
+
try {
|
|
1172
|
+
return await (options?.fetch ?? fetch)(target, this.requestInit(method, method === "POST" ? body : undefined, options, apiKey, linked.signal));
|
|
1173
|
+
} finally {
|
|
1174
|
+
if (timer) clearTimeout(timer);
|
|
1175
|
+
}
|
|
1176
|
+
},
|
|
1177
|
+
onHttpError: (r, text) => this.throwIfError(r.status, r.statusText, target, text, clean, headersToRecord(r.headers)),
|
|
1178
|
+
noBodyError: () => toConciseProviderError(new Error("Google streaming response had no body"), "google", clean),
|
|
1179
|
+
retry: retryOptionsFrom(options),
|
|
1180
|
+
signal: linked.signal,
|
|
1181
|
+
label: { providerId: "google", modelId: clean },
|
|
1182
|
+
});
|
|
1183
|
+
const res = await open(url, "POST");
|
|
985
1184
|
const responseHeaders: Record<string, string> = {};
|
|
986
1185
|
res.headers.forEach((v, k) => {
|
|
987
1186
|
responseHeaders[k] = v;
|
|
@@ -989,13 +1188,14 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
989
1188
|
const responseMeta: Record<string, unknown> & { status: number; statusText: string; headers: Record<string, string> } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
|
|
990
1189
|
eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
|
|
991
1190
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
const decoder = new TextDecoder();
|
|
1191
|
+
let reader: StreamReader | undefined;
|
|
1192
|
+
let lastEventId: string | undefined;
|
|
995
1193
|
let text = "";
|
|
996
1194
|
let thinking = "";
|
|
997
1195
|
let signature: string | undefined;
|
|
998
1196
|
const calls = new Map<number, { id: string; name: string; startArgs: string; deltaArgs: string }>();
|
|
1197
|
+
// Every step as the server would return it (start + deltas), for exact history replay.
|
|
1198
|
+
const steps = new Map<number, Record<string, unknown>>();
|
|
999
1199
|
let usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
1000
1200
|
let finishReason = "stop";
|
|
1001
1201
|
let sawFinishReason = false;
|
|
@@ -1010,11 +1210,14 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1010
1210
|
() => {
|
|
1011
1211
|
aborted = true;
|
|
1012
1212
|
try {
|
|
1013
|
-
void reader
|
|
1213
|
+
void reader?.cancel();
|
|
1014
1214
|
} catch {}
|
|
1015
1215
|
},
|
|
1016
1216
|
{ once: true }
|
|
1017
1217
|
);
|
|
1218
|
+
cancelBackground = () => {
|
|
1219
|
+
if (body.background && responseId) this.cancelInteraction(baseUrl, responseId, options, apiKey);
|
|
1220
|
+
};
|
|
1018
1221
|
|
|
1019
1222
|
const handleMessage = (event: string | undefined, data: string): void => {
|
|
1020
1223
|
if (!data) return;
|
|
@@ -1031,6 +1234,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1031
1234
|
return;
|
|
1032
1235
|
}
|
|
1033
1236
|
const type = (msg["event_type"] as string) || event;
|
|
1237
|
+
if (typeof msg["event_id"] === "string") lastEventId = msg["event_id"] as string;
|
|
1034
1238
|
if (type === "error") {
|
|
1035
1239
|
// SSE error event (e.g. rate_limit_exceeded mid-stream). Must
|
|
1036
1240
|
// surface — silently ending with empty text + zero usage hides it.
|
|
@@ -1050,16 +1254,20 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1050
1254
|
const delta = (msg["delta"] as Record<string, unknown>) ?? {};
|
|
1051
1255
|
const dType = delta["type"] as string;
|
|
1052
1256
|
if (dType === "text" && typeof delta["text"] === "string") {
|
|
1257
|
+
appendBlockText(steps.get(index), "content", delta["text"] as string);
|
|
1053
1258
|
text += delta["text"] as string;
|
|
1054
1259
|
eventStream.push({ type: "text_delta", delta: delta["text"] as string, partialText: text });
|
|
1055
1260
|
} else if (dType === "thought_summary") {
|
|
1056
1261
|
const content = delta["content"] as { type?: string; text?: string } | undefined;
|
|
1057
1262
|
const t = content?.text ?? (typeof delta["text"] === "string" ? (delta["text"] as string) : "");
|
|
1058
1263
|
if (t) {
|
|
1264
|
+
appendBlockText(steps.get(index), "summary", t);
|
|
1059
1265
|
thinking += t;
|
|
1060
1266
|
eventStream.push({ type: "thinking_delta", thinkingDelta: t, partialThinking: thinking });
|
|
1061
1267
|
}
|
|
1062
1268
|
} else if (dType === "thought_signature" && typeof delta["signature"] === "string") {
|
|
1269
|
+
const s = steps.get(index);
|
|
1270
|
+
if (s) s["signature"] = delta["signature"];
|
|
1063
1271
|
// Wire truth: signature arrives as its own delta (no text event).
|
|
1064
1272
|
if (!signature) signature = delta["signature"] as string;
|
|
1065
1273
|
} else if (dType === "arguments" || dType === "arguments_delta") {
|
|
@@ -1073,7 +1281,21 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1073
1281
|
} else if (type === "step.start") {
|
|
1074
1282
|
const index = (msg["index"] as number) ?? 0;
|
|
1075
1283
|
const step = (msg["step"] as Record<string, unknown>) ?? {};
|
|
1076
|
-
|
|
1284
|
+
steps.set(index, step);
|
|
1285
|
+
if (step["type"] === "model_output") {
|
|
1286
|
+
const t = blockText(step["content"]);
|
|
1287
|
+
if (t) {
|
|
1288
|
+
text += t;
|
|
1289
|
+
eventStream.push({ type: "text_delta", delta: t, partialText: text });
|
|
1290
|
+
}
|
|
1291
|
+
} else if (step["type"] === "thought") {
|
|
1292
|
+
const t = blockText(step["summary"]);
|
|
1293
|
+
if (t) {
|
|
1294
|
+
thinking += t;
|
|
1295
|
+
eventStream.push({ type: "thinking_delta", thinkingDelta: t, partialThinking: thinking });
|
|
1296
|
+
}
|
|
1297
|
+
if (!signature && typeof step["signature"] === "string" && step["signature"]) signature = step["signature"];
|
|
1298
|
+
} else if (step["type"] === "function_call") {
|
|
1077
1299
|
const args = step["arguments"];
|
|
1078
1300
|
const startArgs = typeof args === "string" ? args : args ? JSON.stringify(args) : "";
|
|
1079
1301
|
calls.set(index, {
|
|
@@ -1083,6 +1305,13 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1083
1305
|
deltaArgs: "",
|
|
1084
1306
|
});
|
|
1085
1307
|
}
|
|
1308
|
+
} else if (type === "step.stop" || type === "interaction.status_update") {
|
|
1309
|
+
// Cumulative usage so far: keeps usage when the stream is cut before `interaction.completed`.
|
|
1310
|
+
const stepUsage = msg["usage"] as InteractionObject["usage"] | undefined;
|
|
1311
|
+
if (stepUsage) {
|
|
1312
|
+
usage = mapUsage(stepUsage);
|
|
1313
|
+
sawUsage = true;
|
|
1314
|
+
}
|
|
1086
1315
|
} else if (type === "interaction.completed") {
|
|
1087
1316
|
const interaction = (msg["interaction"] as InteractionObject) ?? {};
|
|
1088
1317
|
// Failed/cancelled interactions must surface loudly — never
|
|
@@ -1100,30 +1329,57 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1100
1329
|
}
|
|
1101
1330
|
};
|
|
1102
1331
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1332
|
+
// Reads one connection to its end. A background connection that drops or stalls just ends
|
|
1333
|
+
// (the caller reconnects); provider error events from handleMessage always throw.
|
|
1334
|
+
const readAll = async (response: Response): Promise<void> => {
|
|
1335
|
+
const parser = new SSEParser();
|
|
1336
|
+
const decoder = new TextDecoder();
|
|
1337
|
+
const current = (reader = response.body!.getReader()); // openStreamResponse guarantees a body
|
|
1338
|
+
try {
|
|
1339
|
+
while (true) {
|
|
1340
|
+
if (linked.signal.aborted || eventStream.isCancelled()) {
|
|
1341
|
+
try {
|
|
1342
|
+
await current.cancel();
|
|
1343
|
+
} catch {}
|
|
1344
|
+
throw Object.assign(new Error("Stream aborted"), { name: "AbortError" });
|
|
1345
|
+
}
|
|
1346
|
+
let chunk: { done: boolean; value?: Uint8Array };
|
|
1347
|
+
try {
|
|
1348
|
+
chunk = body.background ? await readWithin(current, BACKGROUND_STREAM_STALL_MS) : await current.read();
|
|
1349
|
+
} catch (err) {
|
|
1350
|
+
if (!body.background || aborted || isAbortError(err)) throw err;
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
1353
|
+
if (chunk.done) break;
|
|
1354
|
+
for (const m of parser.feed(decoder.decode(chunk.value, { stream: true }))) handleMessage(m.event, m.data);
|
|
1355
|
+
}
|
|
1356
|
+
for (const m of parser.flush()) handleMessage(m.event, m.data);
|
|
1357
|
+
} finally {
|
|
1105
1358
|
try {
|
|
1106
|
-
|
|
1359
|
+
current.releaseLock();
|
|
1107
1360
|
} catch {}
|
|
1108
|
-
throw Object.assign(new Error("Stream aborted"), { name: "AbortError" });
|
|
1109
1361
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
await readAll(res);
|
|
1365
|
+
// Long decode: a background stream may drop; resume after the last event until the interaction ends.
|
|
1366
|
+
for (let misses = 0; body.background && completedBody === undefined && !aborted; ) {
|
|
1367
|
+
if (!responseId || ++misses > BACKGROUND_MAX_RECONNECTS) throw incompleteStreamError("google", clean);
|
|
1368
|
+
const before = framesSeen;
|
|
1369
|
+
const resume = `${baseUrl}/interactions/${responseId}?stream=true${lastEventId ? `&last_event_id=${encodeURIComponent(lastEventId)}` : ""}`;
|
|
1370
|
+
await readAll(await open(resume, "GET"));
|
|
1371
|
+
if (framesSeen > before) misses = 0;
|
|
1114
1372
|
}
|
|
1115
|
-
for (const m of parser.flush()) handleMessage(m.event, m.data);
|
|
1116
|
-
try {
|
|
1117
|
-
reader.releaseLock();
|
|
1118
|
-
} catch {}
|
|
1119
1373
|
|
|
1120
1374
|
if (linked.signal.aborted || eventStream.isCancelled() || aborted) {
|
|
1121
1375
|
throw Object.assign(new Error("Stream aborted"), { name: "AbortError" });
|
|
1122
1376
|
}
|
|
1123
1377
|
|
|
1124
1378
|
const toolCalls: ToolCallRecord[] = [];
|
|
1125
|
-
for (const c of calls
|
|
1379
|
+
for (const [index, c] of calls) {
|
|
1126
1380
|
const args = parseStreamedToolArguments(c.startArgs, c.deltaArgs);
|
|
1381
|
+
const step = steps.get(index);
|
|
1382
|
+
if (step) step["arguments"] = args;
|
|
1127
1383
|
const record: ToolCallRecord = {
|
|
1128
1384
|
id: c.id,
|
|
1129
1385
|
name: c.name,
|
|
@@ -1178,6 +1434,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1178
1434
|
response: { ...responseMeta, body: completedBody, badFrames } as unknown as ProviderRawData["response"],
|
|
1179
1435
|
},
|
|
1180
1436
|
durationMs: Date.now() - startTime,
|
|
1437
|
+
providerSteps: replaySteps([...steps.entries()].sort(([a], [b]) => a - b).map(([, s]) => s)),
|
|
1181
1438
|
});
|
|
1182
1439
|
eventStream.push({ type: "done", delta: "", usage, finishReason, responseId });
|
|
1183
1440
|
eventStream.end(finalResponse);
|
|
@@ -1188,6 +1445,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
1188
1445
|
eventStream.isCancelled() ||
|
|
1189
1446
|
(raw as { name?: string }).name === "AbortError";
|
|
1190
1447
|
if (isAbort) {
|
|
1448
|
+
cancelBackground();
|
|
1191
1449
|
eventStream.fail(
|
|
1192
1450
|
Object.assign(new Error("Stream aborted"), { name: "AbortError" })
|
|
1193
1451
|
);
|
|
@@ -90,6 +90,7 @@ import {
|
|
|
90
90
|
mapGenerationParamsToChat,
|
|
91
91
|
normalizeStructuredOutput,
|
|
92
92
|
augmentSystemPromptWithStructuredOutput,
|
|
93
|
+
warnIgnoredMediaResolution,
|
|
93
94
|
} from "../providers.ts";
|
|
94
95
|
|
|
95
96
|
// ---------------------------------------------------------------------------
|
|
@@ -704,6 +705,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
704
705
|
}
|
|
705
706
|
// Strict endpoints reject unknown body fields: implicit caching only.
|
|
706
707
|
resolveCache(this.prefix, options?.cache, ["implicit"], `${this.prefix}/${modelId}`);
|
|
708
|
+
warnIgnoredMediaResolution(this.prefix, context.messages, `${this.prefix}/${modelId}`);
|
|
707
709
|
|
|
708
710
|
// Structured outputs: native `response_format` when supported, else
|
|
709
711
|
// emulated via system prompt (strict endpoints would 400 on json_schema
|
package/src/providers/openai.ts
CHANGED
|
@@ -85,6 +85,7 @@ import {
|
|
|
85
85
|
normalizeStructuredOutput,
|
|
86
86
|
augmentSystemPromptWithStructuredOutput,
|
|
87
87
|
emitProviderWarning,
|
|
88
|
+
warnIgnoredMediaResolution,
|
|
88
89
|
} from "../providers.ts";
|
|
89
90
|
|
|
90
91
|
// ---------------------------------------------------------------------------
|
|
@@ -524,6 +525,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
524
525
|
const serviceTier = mapServiceTierToOpenAI(options?.serviceTier);
|
|
525
526
|
// `12h` maps to `prompt_cache_retention: "24h"`; everything else stays on implicit prompt_cache_key affinity.
|
|
526
527
|
const cache = resolveCache("openai", options?.cache, ["implicit", "12h"], `openai/${modelId}`);
|
|
528
|
+
warnIgnoredMediaResolution("openai", context.messages, `openai/${modelId}`);
|
|
527
529
|
|
|
528
530
|
// Structured outputs: native `text.format` when supported, else emulated
|
|
529
531
|
// via instructions (catalog `structured_output: false` would 400).
|
|
@@ -77,6 +77,7 @@ import {
|
|
|
77
77
|
normalizeStructuredOutput,
|
|
78
78
|
augmentSystemPromptWithStructuredOutput,
|
|
79
79
|
emitProviderWarning,
|
|
80
|
+
warnIgnoredMediaResolution,
|
|
80
81
|
} from "../providers.ts";
|
|
81
82
|
|
|
82
83
|
// ---------------------------------------------------------------------------
|
|
@@ -576,6 +577,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
576
577
|
const serviceTier = mapServiceTierToOpenRouter(options?.serviceTier);
|
|
577
578
|
// Anthropic models take a top-level `cache_control` TTL; every other route caches implicitly (docs: prompt-caching.md).
|
|
578
579
|
const cache = resolveCache("openrouter", options?.cache, modelId.startsWith("anthropic/") ? ["implicit", "5m", "1h"] : ["implicit"], `openrouter/${modelId}`);
|
|
580
|
+
warnIgnoredMediaResolution("openrouter", context.messages, `openrouter/${modelId}`);
|
|
579
581
|
|
|
580
582
|
// Structured outputs: native `response_format` when supported, else
|
|
581
583
|
// emulated via system-prompt instruction + client validation (catalog
|
package/src/providers.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { CacheOption, ServiceTier, ThinkingLevel } from "./types/core.ts";
|
|
13
13
|
import type { StructuredOutputSpec } from "./types/model.ts";
|
|
14
|
+
import type { Message } from "./types/message.ts";
|
|
14
15
|
import { zodToJsonSchema } from "./tools/schema.ts";
|
|
15
16
|
import { ValidationError } from "./types/errors.ts";
|
|
16
17
|
import { TRUNCATED_ARGS_KEY } from "./types/tool.ts";
|
|
@@ -247,6 +248,21 @@ export function resolveCache(
|
|
|
247
248
|
return undefined;
|
|
248
249
|
}
|
|
249
250
|
|
|
251
|
+
/** Warns once per provider/model that per-media `resolution` is ignored (no such control on that wire); in: an adapter's request messages, out: telemetry bus only. */
|
|
252
|
+
export function warnIgnoredMediaResolution(provider: string, messages: Message[], modelRef: string): void {
|
|
253
|
+
const used = messages.some(
|
|
254
|
+
(m) => Array.isArray(m.content) && m.content.some((p) => (p.type === "image" || p.type === "video") && p.resolution)
|
|
255
|
+
);
|
|
256
|
+
if (!used) return;
|
|
257
|
+
emitProviderWarning({
|
|
258
|
+
provider,
|
|
259
|
+
capability: "media resolution",
|
|
260
|
+
requested: modelRef,
|
|
261
|
+
reason: "this provider has no per-media detail control (Google only).",
|
|
262
|
+
fallback: "the provider's default detail",
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
250
266
|
/** OpenRouter Chat Completions `tool_choice` wire values. */
|
|
251
267
|
export type OpenRouterChatToolChoice =
|
|
252
268
|
| "auto"
|
package/src/types/message.ts
CHANGED
|
@@ -9,6 +9,9 @@ export interface TextPart {
|
|
|
9
9
|
thoughtSignature?: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/** Detail level a provider spends on one image or video (more detail = more input tokens). Google only; other providers ignore it with a warning. */
|
|
13
|
+
export type MediaResolution = "low" | "medium" | "high" | "ultra_high";
|
|
14
|
+
|
|
12
15
|
/** Image content accepted as a URL, data, path, or binary value. */
|
|
13
16
|
export interface ImagePart {
|
|
14
17
|
type: "image";
|
|
@@ -17,6 +20,8 @@ export interface ImagePart {
|
|
|
17
20
|
*/
|
|
18
21
|
image: string | Uint8Array | ArrayBuffer;
|
|
19
22
|
mimeType?: string;
|
|
23
|
+
/** Detail level for this image (Google: `low` 280, `medium` 560, `high` 1120, `ultra_high` 2240 tokens). */
|
|
24
|
+
resolution?: MediaResolution;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
/** Audio content accepted as a URL, data, path, or binary value. */
|
|
@@ -48,6 +53,8 @@ export interface VideoPart {
|
|
|
48
53
|
*/
|
|
49
54
|
video: string | Uint8Array | ArrayBuffer;
|
|
50
55
|
mimeType?: string;
|
|
56
|
+
/** Detail level per frame (Google: `low`/`medium` 70, `high` 280 tokens). */
|
|
57
|
+
resolution?: MediaResolution;
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
/** Assistant-generated structured tool invocation. */
|
|
@@ -100,12 +107,20 @@ export type ContentPart =
|
|
|
100
107
|
|
|
101
108
|
export type MessageRole = "system" | "user" | "assistant" | "tool";
|
|
102
109
|
|
|
110
|
+
/** A provider's raw output steps for one assistant turn, kept to replay history byte-exact (only the tagged provider reads them). */
|
|
111
|
+
export interface ProviderSteps {
|
|
112
|
+
provider: string;
|
|
113
|
+
steps: unknown[];
|
|
114
|
+
}
|
|
115
|
+
|
|
103
116
|
/** Normalized conversation message. */
|
|
104
117
|
export interface Message {
|
|
105
118
|
role: MessageRole;
|
|
106
119
|
content: string | ContentPart[];
|
|
107
120
|
name?: string;
|
|
108
121
|
thoughtSignature?: string;
|
|
122
|
+
/** Raw provider output steps of an assistant turn (opaque; e.g. Google `steps`). */
|
|
123
|
+
providerSteps?: ProviderSteps;
|
|
109
124
|
}
|
|
110
125
|
|
|
111
126
|
/**
|
package/src/types/model.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ThinkingConfig, CacheOption, ServiceTier, TokenUsage } from "./core.ts";
|
|
2
|
-
import type { ProviderContext } from "./message.ts";
|
|
2
|
+
import type { ProviderContext, ProviderSteps } from "./message.ts";
|
|
3
3
|
import type { StandardToolDeclaration, ToolCallRecord } from "./tool.ts";
|
|
4
4
|
import type { AssistantMessageEventStream } from "../streaming/event-stream.ts";
|
|
5
5
|
|
|
@@ -226,6 +226,8 @@ export interface ProviderGenerateResult {
|
|
|
226
226
|
provider: ProviderId;
|
|
227
227
|
raw: ProviderRawData;
|
|
228
228
|
durationMs: number;
|
|
229
|
+
/** Raw output steps for exact history replay (stored on the assistant message). */
|
|
230
|
+
providerSteps?: ProviderSteps;
|
|
229
231
|
}
|
|
230
232
|
|
|
231
233
|
/** Provider contract implemented by native REST transports. */
|
|
@@ -244,4 +246,6 @@ export interface Provider {
|
|
|
244
246
|
context: ProviderContext,
|
|
245
247
|
options?: ProviderRequestOptions
|
|
246
248
|
): AssistantMessageEventStream;
|
|
249
|
+
/** True when this call may legitimately run far past the request timeouts (e.g. Google long decode); the loop then leaves timing to the adapter's own per-request timeouts. */
|
|
250
|
+
isLongRunning?(model: string | ModelSpec, options?: ProviderRequestOptions): boolean;
|
|
247
251
|
}
|
package/src/types/response.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TokenUsage } from "./core.ts";
|
|
2
2
|
import type { ToolCallRecord, ToolResultRecord } from "./tool.ts";
|
|
3
3
|
import type { ProviderRawData, ProviderId } from "./model.ts";
|
|
4
|
+
import type { ProviderSteps } from "./message.ts";
|
|
4
5
|
|
|
5
6
|
/** One logged step inside a sub-agent worker run (surfaced via `agent.track(id)`). */
|
|
6
7
|
export interface SubAgentStep {
|
|
@@ -142,6 +143,8 @@ export class AgentResponse {
|
|
|
142
143
|
readonly durationMs: number;
|
|
143
144
|
readonly raw: ProviderRawData;
|
|
144
145
|
readonly turns: number;
|
|
146
|
+
/** Raw provider output steps of a single streamed turn (history replay only; not in `toJSON`). */
|
|
147
|
+
readonly providerSteps?: ProviderSteps;
|
|
145
148
|
|
|
146
149
|
/**
|
|
147
150
|
* Constructs a normalized response. Agent runs create this automatically;
|
|
@@ -167,6 +170,7 @@ export class AgentResponse {
|
|
|
167
170
|
durationMs: number;
|
|
168
171
|
raw: ProviderRawData;
|
|
169
172
|
turns?: number;
|
|
173
|
+
providerSteps?: ProviderSteps;
|
|
170
174
|
}) {
|
|
171
175
|
this.text = data.text;
|
|
172
176
|
if (data.parsed !== undefined) (this as { parsed?: unknown }).parsed = data.parsed;
|
|
@@ -184,6 +188,7 @@ export class AgentResponse {
|
|
|
184
188
|
this.durationMs = data.durationMs;
|
|
185
189
|
this.raw = data.raw;
|
|
186
190
|
this.turns = data.turns ?? 1;
|
|
191
|
+
if (data.providerSteps) this.providerSteps = data.providerSteps;
|
|
187
192
|
}
|
|
188
193
|
|
|
189
194
|
/** Returns the final assistant text. */
|
package/src/utils/headers.ts
CHANGED
|
@@ -23,7 +23,7 @@ export function getPackageVersion(): string {
|
|
|
23
23
|
const v = (globalThis as any).process?.env?.npm_package_version;
|
|
24
24
|
if (typeof v === "string" && v) return v;
|
|
25
25
|
} catch {}
|
|
26
|
-
return "0.5.
|
|
26
|
+
return "0.5.2";
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
function getAgentAccelUserAgent(): string {
|
package/src/utils/retry.ts
CHANGED
|
@@ -43,7 +43,8 @@ function readRetryAfterMs(error: unknown): number | undefined {
|
|
|
43
43
|
return Math.min(60_000, secs * 1000);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
// Waits `ms`, rejecting with an AbortError as soon as `signal` aborts; in: retry backoff and Google background polling, out: the awaiting caller.
|
|
47
|
+
export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
47
48
|
if (ms <= 0) return Promise.resolve();
|
|
48
49
|
return new Promise((resolve, reject) => {
|
|
49
50
|
const timer = setTimeout(() => {
|