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.
@@ -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
- lookupCache.set(cacheKey, result);
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}`) || modelId.endsWith(key) || normalizeModelIdForLookup(key) === normalizeModelIdForLookup(modelId)) {
274
+ if (key === modelId || key.endsWith(`/${modelId}`) || normalizeModelIdForLookup(key) === normalizeModelIdForLookup(modelId)) {
206
275
  result = mapModelSpec(p as ProviderId, key, raw as any);
207
- lookupCache.set(cacheKey, result);
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
- lookupCache.set(cacheKey, result);
289
+ setLookupCache(cacheKey, result);
221
290
  return result;
222
291
  }
223
292
 
224
- lookupCache.set(cacheKey, undefined);
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 \x1b[36m💡 How to fix:\x1b[0m\n ${opts.remedy}`
421
+ ? `\n\n How to fix:\n ${opts.remedy}`
353
422
  : "";
354
423
  const allowedLine = opts.allowedLevels.length > 0
355
- ? `\n \x1b[1mAllowed Levels:\x1b[0m [${opts.allowedLevels.map((l) => `"${l}"`).join(", ")}]`
424
+ ? `\n Allowed Levels: [${opts.allowedLevels.map((l) => `"${l}"`).join(", ")}]`
356
425
  : "";
357
426
 
358
427
  const formattedMessage =
359
- `\x1b[31m[Agent Accelerator] ThinkingLevel Mismatch for "${opts.provider}/${opts.modelId}":\x1b[0m\n` +
360
- ` \x1b[1mRequested Level:\x1b[0m "${opts.requestedLevel}"\n` +
361
- ` \x1b[1mIssue:\x1b[0m ${opts.reason}` +
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")];
@@ -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: part.thoughtSignature || m.thoughtSignature || "",
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
- steps.push({ type: "thought", signature: m.thoughtSignature });
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 || `call_${Math.random().toString(36).slice(2, 9)}`,
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
- function redactedHeaders(headers: Record<string, string>): Record<string, string> {
435
- const out: Record<string, string> = {};
436
- for (const [k, v] of Object.entries(headers)) {
437
- out[k] = k.toLowerCase() === "x-goog-api-key" ? "[REDACTED]" : v;
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
- headers["x-goog-api-key"] = apiKey;
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 res = await fetch(url, this.requestInit(body, options, apiKey, signal));
572
- const text = await res.text();
573
- const headers: Record<string, string> = {};
574
- res.headers.forEach((v, k) => {
575
- headers[k] = v;
576
- });
577
- return { status: res.status, statusText: res.statusText, headers, text };
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 res = await fetch(url, this.requestInit(body, options, apiKey, linked.signal));
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 || data === "[DONE]") return;
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) || `call_${Math.random().toString(36).slice(2, 9)}`,
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) usage = mapUsage(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 cleanThinking = thinking.replace(/\n{3,}/g, "\n\n").trim();
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: { request: rawRequest, response: { ...responseMeta, body: completedBody } },
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 });