agent-accelerator 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/agent/agent.ts +830 -116
- package/src/agent/context.ts +35 -0
- package/src/agent/delegation.ts +390 -36
- package/src/agent/loop.ts +711 -109
- package/src/agent/subagent.ts +59 -0
- package/src/index.ts +154 -59
- package/src/models/catalog-cache.ts +68 -25
- package/src/models/catalog.ts +97 -18
- package/src/providers/google.ts +99 -39
- package/src/providers/openai-compat.ts +140 -80
- package/src/providers/openai.ts +149 -69
- package/src/providers/openrouter.ts +106 -71
- package/src/providers/registry.ts +16 -12
- package/src/providers/shared.ts +132 -0
- package/src/providers.ts +3 -4
- package/src/session/store.ts +512 -33
- package/src/streaming/event-stream.ts +156 -41
- package/src/tools/executor.ts +248 -15
- package/src/tools/tool.ts +147 -3
- package/src/types/agent.ts +229 -2
- package/src/types/errors.ts +133 -0
- package/src/types/model.ts +1 -1
- package/src/types/response.ts +126 -3
- package/src/types/tool.ts +69 -0
- package/src/update-models.ts +8 -2
- package/src/utils/cache.ts +68 -1
- package/src/utils/env.ts +2 -6
- package/src/utils/headers.ts +153 -19
- package/src/utils/retry.ts +27 -13
- package/src/utils/session.ts +284 -0
- package/src/providers/openrouter-responses.ts +0 -949
- package/src/types/provider-payloads.ts +0 -352
package/src/models/catalog.ts
CHANGED
|
@@ -39,12 +39,6 @@ const MODALITY_OVERRIDES: Record<string, { input?: string[]; output?: string[] }
|
|
|
39
39
|
// OpenRouter routes this free model to an endpoint that parses PDFs,
|
|
40
40
|
// verified end-to-end via examples/08. If routing changes, the runtime
|
|
41
41
|
// 404 still surfaces as a one-line error.
|
|
42
|
-
"openrouter/nvidia/nemotron-3.5-lightning:free": { input: ["text", "pdf"] },
|
|
43
|
-
// Live-verified 2026-09-24: `stealth/space-bunny-alpha` accepts PDF
|
|
44
|
-
// `input_file` over Responses and PDFs via the file-parser plugin over Chat
|
|
45
|
-
// Completions (Berkshire letter + sample-files PDF both completed), though
|
|
46
|
-
// models.dev lists text/image/video only.
|
|
47
|
-
"openrouter/stealth/space-bunny-alpha": { input: ["text", "image", "video", "pdf"] },
|
|
48
42
|
};
|
|
49
43
|
|
|
50
44
|
function applyModalityOverrides(provider: string, modelId: string, base: ModelModalities): ModelModalities {
|
|
@@ -68,10 +62,72 @@ function normalizeModelIdForLookup(modelId: string): string {
|
|
|
68
62
|
|
|
69
63
|
// ---------------------------------------------------------------------------
|
|
70
64
|
// Internal cache — battle-tested: avoid repeated global scans (7487 models)
|
|
65
|
+
// Bound lookupCache to 1000 entries LRU (Q-33): evict oldest on overflow.
|
|
71
66
|
// ---------------------------------------------------------------------------
|
|
72
67
|
const lookupCache = new Map<string, ModelSpec | undefined>();
|
|
73
68
|
const providerModelsCache = new Map<string, ModelSpec[]>();
|
|
74
69
|
|
|
70
|
+
/** Bounded LRU set for lookupCache (Q-33): keeps at most 1000 entries. */
|
|
71
|
+
function setLookupCache(key: string, value: ModelSpec | undefined): void {
|
|
72
|
+
if (lookupCache.has(key)) lookupCache.delete(key);
|
|
73
|
+
else if (lookupCache.size >= 1000) {
|
|
74
|
+
const oldest = lookupCache.keys().next();
|
|
75
|
+
if (!oldest.done) lookupCache.delete(oldest.value);
|
|
76
|
+
}
|
|
77
|
+
lookupCache.set(key, value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Custom-registered models (Q-33): checked first in getModelFromCatalog.
|
|
81
|
+
const customModelRegistry = new Map<string, ModelSpec>();
|
|
82
|
+
|
|
83
|
+
function customRegistryKey(provider: string, modelId: string): string {
|
|
84
|
+
return `${provider.toLowerCase()}::${modelId.toLowerCase()}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Registers a custom model spec that takes precedence over the catalog (Q-33).
|
|
89
|
+
* Stored in a separate map, checked first in `getModelFromCatalog`.
|
|
90
|
+
*/
|
|
91
|
+
export function registerModel(spec: ModelSpec): void {
|
|
92
|
+
if (!spec || typeof spec.id !== "string" || typeof spec.provider !== "string") {
|
|
93
|
+
throw new Error("[Agent Accelerator] registerModel requires spec.id and spec.provider strings");
|
|
94
|
+
}
|
|
95
|
+
customModelRegistry.set(customRegistryKey(String(spec.provider), String(spec.id)), { ...spec });
|
|
96
|
+
// Invalidate derived caches so the custom model is visible immediately.
|
|
97
|
+
lookupCache.clear();
|
|
98
|
+
providerModelsCache.clear();
|
|
99
|
+
globalIndex = null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Clears custom-registered models (primarily for tests). */
|
|
103
|
+
export function clearCustomModels(): void {
|
|
104
|
+
customModelRegistry.clear();
|
|
105
|
+
lookupCache.clear();
|
|
106
|
+
providerModelsCache.clear();
|
|
107
|
+
globalIndex = null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Returns where a model resolves from without touching frozen types (Q-33):
|
|
112
|
+
* `"override"` for custom-registered or MODALITY_OVERRIDES-covered models,
|
|
113
|
+
* `"catalog"` for plain catalog hits, `"unknown"` when unresolvable.
|
|
114
|
+
*/
|
|
115
|
+
export function getModelSource(provider: string, modelId: string): "catalog" | "override" | "unknown" {
|
|
116
|
+
const p = String(provider ?? "").toLowerCase();
|
|
117
|
+
const m = String(modelId ?? "");
|
|
118
|
+
const norm = normalizeModelIdForLookup(m);
|
|
119
|
+
if (customModelRegistry.has(customRegistryKey(p, m)) || customModelRegistry.has(customRegistryKey(p, norm))) {
|
|
120
|
+
return "override";
|
|
121
|
+
}
|
|
122
|
+
if (
|
|
123
|
+
MODALITY_OVERRIDES[`${p}/${m}`] !== undefined ||
|
|
124
|
+
MODALITY_OVERRIDES[`${p}/${norm}`] !== undefined
|
|
125
|
+
) {
|
|
126
|
+
return "override";
|
|
127
|
+
}
|
|
128
|
+
return getModelFromCatalog(provider, modelId) ? "catalog" : "unknown";
|
|
129
|
+
}
|
|
130
|
+
|
|
75
131
|
function mapCapabilities(raw: any, cost: ModelCost, reasoning: boolean, toolCall: boolean, modalities: ModelModalities) {
|
|
76
132
|
return {
|
|
77
133
|
supportsThinking: reasoning,
|
|
@@ -180,6 +236,16 @@ export function getModelFromCatalog(providerInput: string, modelIdInput: string)
|
|
|
180
236
|
const modelId = modelIdInput.trim();
|
|
181
237
|
let result: ModelSpec | undefined;
|
|
182
238
|
|
|
239
|
+
// Q-33: custom-registered models win over the catalog.
|
|
240
|
+
const customHit =
|
|
241
|
+
customModelRegistry.get(customRegistryKey(provider, modelId)) ??
|
|
242
|
+
customModelRegistry.get(customRegistryKey(provider, normalizeModelIdForLookup(modelId)));
|
|
243
|
+
if (customHit) {
|
|
244
|
+
result = { ...customHit };
|
|
245
|
+
setLookupCache(cacheKey, result);
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
|
|
183
249
|
const aliasProviders = PROVIDER_ALIASES[provider] || [provider];
|
|
184
250
|
const tryProviders = [...new Set([...aliasProviders, provider, provider.replace("-zen", ""), provider.replace("-go", "")])];
|
|
185
251
|
const catalog = getActiveCatalog();
|
|
@@ -197,14 +263,17 @@ export function getModelFromCatalog(providerInput: string, modelIdInput: string)
|
|
|
197
263
|
const raw = providerData.models[cand];
|
|
198
264
|
if (raw) {
|
|
199
265
|
result = mapModelSpec(p as ProviderId, cand, raw);
|
|
200
|
-
|
|
266
|
+
setLookupCache(cacheKey, result);
|
|
201
267
|
return result;
|
|
202
268
|
}
|
|
203
269
|
}
|
|
270
|
+
// Q-33: tightened fuzzy match — exact, `endsWith("/"+modelId)` boundary,
|
|
271
|
+
// or normalized equality. The old loose `modelId.endsWith(key)` is removed
|
|
272
|
+
// (e.g. "gpt-4o-mini-extra" must not match "mini").
|
|
204
273
|
for (const [key, raw] of Object.entries(providerData.models as Record<string, any>)) {
|
|
205
|
-
if (key === modelId || key.endsWith(`/${modelId}`) ||
|
|
274
|
+
if (key === modelId || key.endsWith(`/${modelId}`) || normalizeModelIdForLookup(key) === normalizeModelIdForLookup(modelId)) {
|
|
206
275
|
result = mapModelSpec(p as ProviderId, key, raw as any);
|
|
207
|
-
|
|
276
|
+
setLookupCache(cacheKey, result);
|
|
208
277
|
return result;
|
|
209
278
|
}
|
|
210
279
|
}
|
|
@@ -217,11 +286,11 @@ export function getModelFromCatalog(providerInput: string, modelIdInput: string)
|
|
|
217
286
|
const hit = index.get(lowerModelId) || index.get(strippedLower);
|
|
218
287
|
if (hit) {
|
|
219
288
|
result = mapModelSpec(hit.provider, hit.key, hit.raw);
|
|
220
|
-
|
|
289
|
+
setLookupCache(cacheKey, result);
|
|
221
290
|
return result;
|
|
222
291
|
}
|
|
223
292
|
|
|
224
|
-
|
|
293
|
+
setLookupCache(cacheKey, undefined);
|
|
225
294
|
return undefined;
|
|
226
295
|
}
|
|
227
296
|
|
|
@@ -349,16 +418,16 @@ export class ThinkingLevelError extends Error {
|
|
|
349
418
|
remedy?: string;
|
|
350
419
|
}) {
|
|
351
420
|
const remedyLines = opts.remedy
|
|
352
|
-
? `\n\n
|
|
421
|
+
? `\n\n How to fix:\n ${opts.remedy}`
|
|
353
422
|
: "";
|
|
354
423
|
const allowedLine = opts.allowedLevels.length > 0
|
|
355
|
-
? `\n
|
|
424
|
+
? `\n Allowed Levels: [${opts.allowedLevels.map((l) => `"${l}"`).join(", ")}]`
|
|
356
425
|
: "";
|
|
357
426
|
|
|
358
427
|
const formattedMessage =
|
|
359
|
-
|
|
360
|
-
`
|
|
361
|
-
`
|
|
428
|
+
`[Agent Accelerator] ThinkingLevel Mismatch for "${opts.provider}/${opts.modelId}":\n` +
|
|
429
|
+
` Requested Level: "${opts.requestedLevel}"\n` +
|
|
430
|
+
` Issue: ${opts.reason}` +
|
|
362
431
|
allowedLine +
|
|
363
432
|
remedyLines;
|
|
364
433
|
|
|
@@ -454,7 +523,7 @@ export function validateModelThinking(provider: string, modelId: string, request
|
|
|
454
523
|
* @example `const googleModels = getModelsForProvider("google");`
|
|
455
524
|
*/
|
|
456
525
|
export function getModelsForProvider(provider: string): ModelSpec[] {
|
|
457
|
-
if (providerModelsCache.has(provider)) return providerModelsCache.get(provider)
|
|
526
|
+
if (providerModelsCache.has(provider)) return [...providerModelsCache.get(provider)!];
|
|
458
527
|
const aliases = PROVIDER_ALIASES[provider] || [provider];
|
|
459
528
|
const seen = new Set<string>();
|
|
460
529
|
const out: ModelSpec[] = [];
|
|
@@ -468,9 +537,19 @@ export function getModelsForProvider(provider: string): ModelSpec[] {
|
|
|
468
537
|
out.push(mapModelSpec(alias as ProviderId, key, raw));
|
|
469
538
|
}
|
|
470
539
|
}
|
|
540
|
+
// Q-33: include custom-registered models for this provider/alias set.
|
|
541
|
+
for (const spec of customModelRegistry.values()) {
|
|
542
|
+
if (aliases.includes(String(spec.provider)) || String(spec.provider) === provider) {
|
|
543
|
+
if (!seen.has(spec.id)) {
|
|
544
|
+
seen.add(spec.id);
|
|
545
|
+
out.push({ ...spec });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
471
549
|
// Also include our internal provider id mapping
|
|
550
|
+
// Q-33: return copies — cache the canonical array, hand out a fresh copy.
|
|
472
551
|
providerModelsCache.set(provider, out);
|
|
473
|
-
return out;
|
|
552
|
+
return [...out];
|
|
474
553
|
}
|
|
475
554
|
|
|
476
555
|
export const GOOGLE_MODELS: ModelSpec[] = [...getModelsForProvider("google")];
|
package/src/providers/google.ts
CHANGED
|
@@ -40,6 +40,15 @@ import { safeStringify } from "../utils/serialization.ts";
|
|
|
40
40
|
import { stripSchemaForGoogle } from "../tools/schema.ts";
|
|
41
41
|
import { toConciseProviderError, assertModalitiesSupported } from "../utils/errors.ts";
|
|
42
42
|
import { withRetries } from "../utils/retry.ts";
|
|
43
|
+
import { setHeaderCaseInsensitive, shouldEchoSignature } from "../utils/headers.ts";
|
|
44
|
+
import {
|
|
45
|
+
newToolCallId,
|
|
46
|
+
redactedHeaders,
|
|
47
|
+
readErrorPayload,
|
|
48
|
+
incompleteStreamError,
|
|
49
|
+
combinedSignal,
|
|
50
|
+
timeoutFor,
|
|
51
|
+
} from "./shared.ts";
|
|
43
52
|
import { createGenericModelSpec } from "../models/catalog.ts";
|
|
44
53
|
import { getModelFromCatalog, getModelsForProvider } from "../models/catalog.ts";
|
|
45
54
|
import {
|
|
@@ -304,9 +313,16 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
304
313
|
const texts: string[] = [];
|
|
305
314
|
for (const part of m.content) {
|
|
306
315
|
if (part.type === "thinking" && part.thinking) {
|
|
316
|
+
// Q-18: preserve namespaced signatures only — foreign-provider
|
|
317
|
+
// signatures never echo here; untagged legacy echoes (compat).
|
|
318
|
+
const partProvider = (part as { thoughtSignatureProvider?: unknown }).thoughtSignatureProvider;
|
|
319
|
+
const msgProvider = (m as { thoughtSignatureProvider?: unknown }).thoughtSignatureProvider;
|
|
320
|
+
const sig = shouldEchoSignature(partProvider ?? msgProvider, "google")
|
|
321
|
+
? part.thoughtSignature || m.thoughtSignature || ""
|
|
322
|
+
: "";
|
|
307
323
|
steps.push({
|
|
308
324
|
type: "thought",
|
|
309
|
-
signature:
|
|
325
|
+
signature: sig,
|
|
310
326
|
summary: [{ type: "text", text: part.thinking }],
|
|
311
327
|
});
|
|
312
328
|
} else if (part.type === "tool_call") {
|
|
@@ -328,7 +344,11 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
|
|
|
328
344
|
} else if (m.thoughtSignature && !steps.some((s) => s.type === "thought")) {
|
|
329
345
|
// Preserve a signature-only thought so stateless chaining keeps
|
|
330
346
|
// reasoning continuity even when no summary text was retained.
|
|
331
|
-
|
|
347
|
+
// Q-18: only for google/untagged signatures.
|
|
348
|
+
const msgProvider = (m as { thoughtSignatureProvider?: unknown }).thoughtSignatureProvider;
|
|
349
|
+
if (shouldEchoSignature(msgProvider, "google")) {
|
|
350
|
+
steps.push({ type: "thought", signature: m.thoughtSignature });
|
|
351
|
+
}
|
|
332
352
|
}
|
|
333
353
|
} else if (m.role === "tool") {
|
|
334
354
|
if (!Array.isArray(m.content)) continue;
|
|
@@ -408,7 +428,7 @@ function parseInteraction(
|
|
|
408
428
|
const args =
|
|
409
429
|
step.arguments && typeof step.arguments === "object" ? step.arguments : { raw: step.arguments };
|
|
410
430
|
toolCalls.push({
|
|
411
|
-
id: step.id ||
|
|
431
|
+
id: step.id || newToolCallId(),
|
|
412
432
|
name: step.name || "unknown",
|
|
413
433
|
arguments: args as Record<string, unknown>,
|
|
414
434
|
rawArguments: JSON.stringify(args),
|
|
@@ -431,26 +451,11 @@ function parseInteraction(
|
|
|
431
451
|
};
|
|
432
452
|
}
|
|
433
453
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
return out;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function readErrorPayload(bodyText: string): { message: string; code?: string | number } {
|
|
443
|
-
try {
|
|
444
|
-
const parsed: unknown = JSON.parse(bodyText);
|
|
445
|
-
const first = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
446
|
-
const err = (first as { error?: { message?: string; code?: string | number; status?: string } })?.error;
|
|
447
|
-
if (err && typeof err.message === "string") {
|
|
448
|
-
return { message: err.message, code: err.code ?? err.status };
|
|
449
|
-
}
|
|
450
|
-
return { message: bodyText.slice(0, 300) };
|
|
451
|
-
} catch {
|
|
452
|
-
return { message: bodyText.slice(0, 300) };
|
|
453
|
-
}
|
|
454
|
+
/** Local per-provider signature tag (Q-18): google history preserves only
|
|
455
|
+
* google/untagged signatures; foreign-provider signatures never echo here. */
|
|
456
|
+
function withGoogleSignature(sig: string | undefined): { thoughtSignature?: string; thoughtSignatureProvider?: string } {
|
|
457
|
+
if (!sig) return {};
|
|
458
|
+
return { thoughtSignature: sig, thoughtSignatureProvider: "google" };
|
|
454
459
|
}
|
|
455
460
|
|
|
456
461
|
// ---------------------------------------------------------------------------
|
|
@@ -557,7 +562,10 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
557
562
|
for (const [k, v] of Object.entries(options?.headers ?? {})) {
|
|
558
563
|
if (!INTERNAL_HEADERS.has(k.toLowerCase())) headers[k] = v;
|
|
559
564
|
}
|
|
560
|
-
|
|
565
|
+
// Q-17: key travels as the `x-goog-api-key` header (never `?key=` in the
|
|
566
|
+
// URL, which would leak into logs). Case-insensitive merge so a custom
|
|
567
|
+
// `X-Goog-Api-Key` never duplicates the canonical header.
|
|
568
|
+
setHeaderCaseInsensitive(headers, "x-goog-api-key", apiKey);
|
|
561
569
|
return { method: "POST", headers, body: JSON.stringify(body), signal };
|
|
562
570
|
}
|
|
563
571
|
|
|
@@ -568,13 +576,18 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
568
576
|
apiKey: string,
|
|
569
577
|
signal?: AbortSignal
|
|
570
578
|
): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
headers
|
|
576
|
-
|
|
577
|
-
|
|
579
|
+
const { signal: effective, cleanup } = combinedSignal(signal, timeoutFor(options as { timeoutMs?: number } | undefined));
|
|
580
|
+
try {
|
|
581
|
+
const res = await fetch(url, this.requestInit(body, options, apiKey, effective));
|
|
582
|
+
const text = await res.text();
|
|
583
|
+
const headers: Record<string, string> = {};
|
|
584
|
+
res.headers.forEach((v, k) => {
|
|
585
|
+
headers[k] = v;
|
|
586
|
+
});
|
|
587
|
+
return { status: res.status, statusText: res.statusText, headers, text };
|
|
588
|
+
} finally {
|
|
589
|
+
cleanup();
|
|
590
|
+
}
|
|
578
591
|
}
|
|
579
592
|
|
|
580
593
|
private throwIfError(
|
|
@@ -585,13 +598,14 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
585
598
|
modelId: string
|
|
586
599
|
): void {
|
|
587
600
|
if (status >= 200 && status < 300) return;
|
|
588
|
-
const { message, code } = readErrorPayload(bodyText);
|
|
601
|
+
const { message, code, errorType } = readErrorPayload(bodyText);
|
|
589
602
|
const err: Record<string, unknown> & Error = new Error(message) as Record<string, unknown> & Error;
|
|
590
603
|
(err as Record<string, unknown>)["statusCode"] = status;
|
|
591
604
|
(err as Record<string, unknown>)["status"] = status;
|
|
592
605
|
(err as Record<string, unknown>)["responseBody"] = bodyText.slice(0, 500);
|
|
593
606
|
(err as Record<string, unknown>)["url"] = url.split("?")[0];
|
|
594
607
|
if (code !== undefined) (err as Record<string, unknown>)["code"] = code;
|
|
608
|
+
if (errorType) (err as Record<string, unknown>)["errorType"] = errorType;
|
|
595
609
|
throw toConciseProviderError(err, "google", modelId);
|
|
596
610
|
}
|
|
597
611
|
|
|
@@ -733,7 +747,21 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
733
747
|
body,
|
|
734
748
|
};
|
|
735
749
|
|
|
736
|
-
const
|
|
750
|
+
const streamTimeoutMs = timeoutFor(options as { timeoutMs?: number } | undefined);
|
|
751
|
+
const streamTimeoutTimer =
|
|
752
|
+
streamTimeoutMs && streamTimeoutMs > 0
|
|
753
|
+
? setTimeout(() => {
|
|
754
|
+
try {
|
|
755
|
+
linked.abort();
|
|
756
|
+
} catch {}
|
|
757
|
+
}, streamTimeoutMs)
|
|
758
|
+
: undefined;
|
|
759
|
+
let res: Response;
|
|
760
|
+
try {
|
|
761
|
+
res = await fetch(url, this.requestInit(body, options, apiKey, linked.signal));
|
|
762
|
+
} finally {
|
|
763
|
+
if (streamTimeoutTimer) clearTimeout(streamTimeoutTimer);
|
|
764
|
+
}
|
|
737
765
|
if (!res.ok || !res.body) {
|
|
738
766
|
const text = !res.ok ? await res.text().catch(() => "") : "";
|
|
739
767
|
if (!res.ok) this.throwIfError(res.status, res.statusText, url, text, clean);
|
|
@@ -743,7 +771,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
743
771
|
res.headers.forEach((v, k) => {
|
|
744
772
|
responseHeaders[k] = v;
|
|
745
773
|
});
|
|
746
|
-
const responseMeta = { status: res.status, statusText: res.statusText, headers: responseHeaders };
|
|
774
|
+
const responseMeta: Record<string, unknown> & { status: number; statusText: string; headers: Record<string, string> } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
|
|
747
775
|
eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
|
|
748
776
|
|
|
749
777
|
const parser = new SSEParser();
|
|
@@ -755,6 +783,10 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
755
783
|
const calls = new Map<number, { id: string; name: string; startArgs: string; deltaArgs: string }>();
|
|
756
784
|
let usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
757
785
|
let finishReason = "stop";
|
|
786
|
+
let sawFinishReason = false;
|
|
787
|
+
let sawUsage = false;
|
|
788
|
+
let badFrames = 0;
|
|
789
|
+
let framesSeen = 0;
|
|
758
790
|
let responseId: string | undefined;
|
|
759
791
|
let completedBody: unknown = undefined;
|
|
760
792
|
let aborted = false;
|
|
@@ -770,11 +802,17 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
770
802
|
);
|
|
771
803
|
|
|
772
804
|
const handleMessage = (event: string | undefined, data: string): void => {
|
|
773
|
-
if (!data
|
|
805
|
+
if (!data) return;
|
|
806
|
+
if (data === "[DONE]") {
|
|
807
|
+
sawFinishReason = sawFinishReason || completedBody !== undefined || sawUsage;
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
774
810
|
let msg: Record<string, unknown>;
|
|
775
811
|
try {
|
|
776
812
|
msg = JSON.parse(data) as Record<string, unknown>;
|
|
813
|
+
framesSeen += 1;
|
|
777
814
|
} catch {
|
|
815
|
+
badFrames += 1;
|
|
778
816
|
return;
|
|
779
817
|
}
|
|
780
818
|
const type = (msg["event_type"] as string) || event;
|
|
@@ -826,7 +864,7 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
826
864
|
const args = step["arguments"];
|
|
827
865
|
const startArgs = typeof args === "string" ? args : args ? JSON.stringify(args) : "";
|
|
828
866
|
calls.set(index, {
|
|
829
|
-
id: (step["id"] as string) ||
|
|
867
|
+
id: (step["id"] as string) || newToolCallId(),
|
|
830
868
|
name: (step["name"] as string) || "unknown",
|
|
831
869
|
startArgs,
|
|
832
870
|
deltaArgs: "",
|
|
@@ -836,8 +874,12 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
836
874
|
const interaction = (msg["interaction"] as InteractionObject) ?? {};
|
|
837
875
|
completedBody = msg["interaction"];
|
|
838
876
|
if (interaction.id) responseId = interaction.id;
|
|
839
|
-
if (interaction.usage)
|
|
877
|
+
if (interaction.usage) {
|
|
878
|
+
usage = mapUsage(interaction.usage);
|
|
879
|
+
sawUsage = true;
|
|
880
|
+
}
|
|
840
881
|
finishReason = mapFinishReason(interaction.status);
|
|
882
|
+
sawFinishReason = true;
|
|
841
883
|
eventStream.push({ type: "usage", usage });
|
|
842
884
|
}
|
|
843
885
|
};
|
|
@@ -882,10 +924,25 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
882
924
|
}
|
|
883
925
|
noteProviderTurn(sessionId, "google");
|
|
884
926
|
|
|
927
|
+
// Q-23: terminal-state guard — fail `incomplete_stream` (retryable)
|
|
928
|
+
// instead of resolving empty success. Skipped for non-SSE payloads
|
|
929
|
+
// (JSON mocks) where no SSE frames were ever seen.
|
|
930
|
+
const hasTerminal = completedBody !== undefined || sawFinishReason || sawUsage;
|
|
885
931
|
// Trim provider trailing blank lines from finalized thinking (live
|
|
886
932
|
// deltas already emitted raw; display collapse happens in the
|
|
887
933
|
// wrapThinking presentation layer). Stateful bodies unaffected.
|
|
888
|
-
const
|
|
934
|
+
const cleanThinkingEarly = thinking.replace(/\n{3,}/g, "\n\n").trim();
|
|
935
|
+
const ctEntry = Object.entries(responseHeaders).find(([k]) => k.toLowerCase() === "content-type");
|
|
936
|
+
const isSSEPayload =
|
|
937
|
+
(typeof ctEntry?.[1] === "string" && ((ctEntry[1] as string).includes("event-stream") || (ctEntry[1] as string).startsWith("text/"))) ||
|
|
938
|
+
framesSeen > 0 ||
|
|
939
|
+
badFrames > 0;
|
|
940
|
+
if (!hasTerminal && !text && !cleanThinkingEarly && toolCalls.length === 0 && isSSEPayload) {
|
|
941
|
+
eventStream.fail(incompleteStreamError("google", clean));
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const cleanThinking = cleanThinkingEarly;
|
|
889
946
|
const finalResponse = new AgentResponse({
|
|
890
947
|
text,
|
|
891
948
|
thinking: cleanThinking || undefined,
|
|
@@ -896,7 +953,10 @@ export class GoogleInteractionsProvider implements Provider {
|
|
|
896
953
|
responseId,
|
|
897
954
|
model: clean,
|
|
898
955
|
provider: "google",
|
|
899
|
-
raw: {
|
|
956
|
+
raw: {
|
|
957
|
+
request: rawRequest,
|
|
958
|
+
response: { ...responseMeta, body: completedBody, badFrames } as unknown as ProviderRawData["response"],
|
|
959
|
+
},
|
|
900
960
|
durationMs: Date.now() - startTime,
|
|
901
961
|
});
|
|
902
962
|
eventStream.push({ type: "done", delta: "", usage, finishReason, responseId });
|