agent-accelerator 0.5.9 → 0.6.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.
Files changed (67) hide show
  1. package/README.md +3 -1
  2. package/examples/prompts/SYSTEM_PROMPT.md +2 -0
  3. package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
  4. package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
  5. package/package.json +4 -1
  6. package/src/agent/agent.ts +366 -161
  7. package/src/agent/architecture.md +13 -12
  8. package/src/agent/config-blocks.ts +39 -12
  9. package/src/agent/context.ts +8 -3
  10. package/src/agent/delegation.ts +82 -29
  11. package/src/agent/loop.ts +444 -365
  12. package/src/agent/subagent.ts +1 -3
  13. package/src/agent/subagents-tool.ts +45 -7
  14. package/src/agent/subagents.md +3 -0
  15. package/src/agent/turns.ts +3 -1
  16. package/src/agent/worker.ts +56 -27
  17. package/src/agent/workers.ts +113 -20
  18. package/src/architecture.md +16 -15
  19. package/src/index.ts +34 -10
  20. package/src/models/architecture.md +4 -3
  21. package/src/models/catalog-cache.ts +6 -5
  22. package/src/models/catalog.ts +39 -30
  23. package/src/providers/architecture.md +10 -8
  24. package/src/providers/google.ts +147 -59
  25. package/src/providers/openai-compat.ts +57 -49
  26. package/src/providers/openai.ts +54 -37
  27. package/src/providers/openrouter.ts +85 -30
  28. package/src/providers/registry.ts +6 -21
  29. package/src/providers/shared.ts +87 -33
  30. package/src/providers.ts +56 -115
  31. package/src/scheduler/architecture.md +7 -6
  32. package/src/scheduler/scheduled-actions.md +4 -0
  33. package/src/scheduler/scheduler.ts +186 -60
  34. package/src/scheduler/store.ts +23 -10
  35. package/src/scheduler/time.ts +17 -3
  36. package/src/scheduler/tool.ts +33 -7
  37. package/src/session/architecture.md +7 -6
  38. package/src/session/journal.ts +38 -12
  39. package/src/session/store.ts +122 -61
  40. package/src/session/usage.ts +58 -9
  41. package/src/streaming/architecture.md +4 -3
  42. package/src/streaming/event-stream.ts +26 -12
  43. package/src/telemetry.ts +6 -2
  44. package/src/tools/architecture.md +8 -7
  45. package/src/tools/executor.ts +50 -48
  46. package/src/tools/schema.ts +10 -6
  47. package/src/tools/subagent-result.ts +6 -1
  48. package/src/tools/tool.ts +9 -11
  49. package/src/types/agent.ts +2 -21
  50. package/src/types/architecture.md +11 -10
  51. package/src/types/core.ts +1 -9
  52. package/src/types/errors.ts +26 -4
  53. package/src/types/model.ts +0 -4
  54. package/src/types/tool.ts +12 -2
  55. package/src/update-models.ts +5 -5
  56. package/src/utils/architecture.md +16 -15
  57. package/src/utils/cache.ts +1 -3
  58. package/src/utils/durable.ts +17 -5
  59. package/src/utils/env.ts +9 -7
  60. package/src/utils/errors.ts +6 -14
  61. package/src/utils/headers.ts +3 -10
  62. package/src/utils/media.ts +23 -8
  63. package/src/utils/raw-capture.ts +9 -2
  64. package/src/utils/retry.ts +15 -3
  65. package/src/utils/safe-io.ts +29 -7
  66. package/src/utils/serialization.ts +1 -4
  67. package/src/utils/session.ts +1 -2
@@ -29,7 +29,7 @@ import type {
29
29
  ProviderGenerateResult,
30
30
  ProviderRawData,
31
31
  } from "../types/model.ts";
32
- import type { ProviderContext, Message, ContentPart, ProviderSteps, ToolResultPart, MediaResolution } from "../types/message.ts";
32
+ import type { ProviderContext, 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";
@@ -53,7 +53,6 @@ import {
53
53
  combinedSignal,
54
54
  timeoutFor,
55
55
  hasAuthHeader,
56
- clampToolCallId,
57
56
  openStreamResponse,
58
57
  retryOptionsFrom,
59
58
  headersToRecord,
@@ -85,19 +84,70 @@ import {
85
84
  // ---------------------------------------------------------------------------
86
85
 
87
86
  /** Image MIME types Google accepts inline. */
88
- export const GOOGLE_IMAGE_MIME = ["image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", "image/gif", "image/bmp", "image/tiff"] as const;
87
+ export const GOOGLE_IMAGE_MIME = [
88
+ "image/png",
89
+ "image/jpeg",
90
+ "image/webp",
91
+ "image/heic",
92
+ "image/heif",
93
+ "image/gif",
94
+ "image/bmp",
95
+ "image/tiff",
96
+ ] as const;
89
97
  /** Audio MIME types Google accepts inline (audio is never fetched by URL). */
90
- 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;
98
+ export const GOOGLE_AUDIO_MIME = [
99
+ "audio/wav",
100
+ "audio/mp3",
101
+ "audio/aiff",
102
+ "audio/aac",
103
+ "audio/ogg",
104
+ "audio/flac",
105
+ "audio/mpeg",
106
+ "audio/m4a",
107
+ "audio/l16",
108
+ "audio/opus",
109
+ "audio/alaw",
110
+ "audio/mulaw",
111
+ "audio/webm",
112
+ ] as const;
91
113
  /** Video MIME types Google accepts inline. */
92
- 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;
114
+ export const GOOGLE_VIDEO_MIME = [
115
+ "video/mp4",
116
+ "video/mpeg",
117
+ "video/mpg",
118
+ "video/mov",
119
+ "video/avi",
120
+ "video/x-flv",
121
+ "video/webm",
122
+ "video/wmv",
123
+ "video/3gpp",
124
+ ] as const;
93
125
  /** Document MIME types Google accepts (the only two). */
94
126
  export const GOOGLE_DOCUMENT_MIME = ["application/pdf", "text/csv"] as const;
95
127
  /** MIME types Google fetches itself from a public `https://` URL. */
96
128
  export const GOOGLE_URL_MIME = [
97
- "text/html", "text/css", "text/plain", "text/xml", "text/csv", "text/rtf", "text/javascript",
98
- "application/json", "application/pdf",
99
- "image/bmp", "image/jpeg", "image/png", "image/webp",
100
- "video/mp4", "video/mpeg", "video/quicktime", "video/avi", "video/x-flv", "video/mpg", "video/webm", "video/wmv", "video/3gpp",
129
+ "text/html",
130
+ "text/css",
131
+ "text/plain",
132
+ "text/xml",
133
+ "text/csv",
134
+ "text/rtf",
135
+ "text/javascript",
136
+ "application/json",
137
+ "application/pdf",
138
+ "image/bmp",
139
+ "image/jpeg",
140
+ "image/png",
141
+ "image/webp",
142
+ "video/mp4",
143
+ "video/mpeg",
144
+ "video/quicktime",
145
+ "video/avi",
146
+ "video/x-flv",
147
+ "video/mpg",
148
+ "video/webm",
149
+ "video/wmv",
150
+ "video/3gpp",
101
151
  ] as const;
102
152
  /** Common MIME spellings mapped to Google's inline names. */
103
153
  export const GOOGLE_INLINE_MIME_ALIASES: Readonly<Record<string, string>> = {
@@ -174,7 +224,13 @@ const YOUTUBE_URL = /^https:\/\/(www\.|m\.)?(youtube\.com|youtu\.be)\//i;
174
224
  // Interactions wire shapes (subset used by this adapter)
175
225
  // ---------------------------------------------------------------------------
176
226
 
177
- type MediaBlock = { type: MediaBlockType; mime_type?: string; data?: string; uri?: string; resolution?: MediaResolution };
227
+ type MediaBlock = {
228
+ type: MediaBlockType;
229
+ mime_type?: string;
230
+ data?: string;
231
+ uri?: string;
232
+ resolution?: MediaResolution;
233
+ };
178
234
  type InteractionContentBlock = { type: "text"; text: string } | MediaBlock;
179
235
 
180
236
  type InteractionInputStep =
@@ -317,7 +373,10 @@ function liveChainFor(sessionId: string | undefined, options?: ProviderRequestOp
317
373
  * below the previous turn's total (Rule 3: a caller dropped the chain without
318
374
  * rehydrating history). Never throws; observability-only.
319
375
  */
320
- function checkTurnTokens(sessionId: string | undefined, usage: { inputTokens: number; outputTokens: number; cachedTokens?: number }): void {
376
+ function checkTurnTokens(
377
+ sessionId: string | undefined,
378
+ usage: { inputTokens: number; outputTokens: number; cachedTokens?: number }
379
+ ): void {
321
380
  try {
322
381
  if (!sessionId) return;
323
382
  const total = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
@@ -409,9 +468,7 @@ function toGoogleTools(tools?: StandardToolDeclaration[]): Array<Record<string,
409
468
  type: "function",
410
469
  name: t.name,
411
470
  description: t.description,
412
- parameters: stripSchemaForGoogle(
413
- (t.parameters || { type: "object", properties: {} }) as Record<string, unknown>
414
- ),
471
+ parameters: stripSchemaForGoogle((t.parameters || { type: "object", properties: {} }) as Record<string, unknown>),
415
472
  }));
416
473
  }
417
474
 
@@ -443,7 +500,9 @@ function uriBlock(type: MediaBlockType, value: string, mimeType?: string): Media
443
500
  if (type === "video" && YOUTUBE_URL.test(value)) return withMime(explicit);
444
501
  const mime = explicit ?? bareMime(inferMimeType(value, ""));
445
502
  if (value.startsWith(FILES_API_PREFIX) || value.startsWith("gs://")) return withMime(mime);
446
- return value.startsWith("https://") && mime && (GOOGLE_URL_MIME as readonly string[]).includes(mime) ? withMime(mime) : undefined;
503
+ return value.startsWith("https://") && mime && (GOOGLE_URL_MIME as readonly string[]).includes(mime)
504
+ ? withMime(mime)
505
+ : undefined;
447
506
  }
448
507
 
449
508
  // Inlines a media part as base64 under Google's MIME name, failing before the network on an unsupported type; in: any media value (bytes, data URL, path, http URL), out: contentPartsToBlocks.
@@ -452,7 +511,8 @@ async function inlineBlock(type: MediaBlockType, value: MediaValue, mimeType?: s
452
511
  const raw = bareMime(norm.mimeType) ?? "";
453
512
  const mime = GOOGLE_INLINE_MIME_ALIASES[raw] ?? raw;
454
513
  if (!INLINE_MIME[type].includes(mime)) {
455
- const what = type === "document" ? "Google reads only PDF and CSV documents" : `Google does not accept ${type} type "${mime}"`;
514
+ const what =
515
+ type === "document" ? "Google reads only PDF and CSV documents" : `Google does not accept ${type} type "${mime}"`;
456
516
  throw new ValidationError(
457
517
  `[Agent Accelerator] ${what} (got "${mime}"; supported: ${INLINE_MIME[type].join(", ")}). Convert the file or pass the correct mimeType.`
458
518
  );
@@ -534,7 +594,9 @@ function functionResultStep(part: ToolResultPart): InteractionInputStep {
534
594
  * behind them. Sending only the last message would drop the tool results and
535
595
  * leave the chained interaction waiting on its function calls.
536
596
  */
537
- async function latestTurnInput(context: ProviderContext): Promise<string | InteractionContentBlock[] | InteractionInputStep[]> {
597
+ async function latestTurnInput(
598
+ context: ProviderContext
599
+ ): Promise<string | InteractionContentBlock[] | InteractionInputStep[]> {
538
600
  const messages = context.messages;
539
601
  let lastAssistant = -1;
540
602
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -617,10 +679,11 @@ async function fullHistorySteps(context: ProviderContext): Promise<InteractionIn
617
679
  const sig = shouldEchoSignature(partProvider ?? msgProvider, "google")
618
680
  ? part.thoughtSignature || m.thoughtSignature || ""
619
681
  : "";
620
- const thoughtStep: { type: "thought"; signature?: string; summary: Array<{ type: string; text?: string }> } = {
621
- type: "thought",
622
- summary: [{ type: "text", text: part.thinking }],
623
- };
682
+ const thoughtStep: { type: "thought"; signature?: string; summary: Array<{ type: string; text?: string }> } =
683
+ {
684
+ type: "thought",
685
+ summary: [{ type: "text", text: part.thinking }],
686
+ };
624
687
  if (sig) thoughtStep.signature = sig;
625
688
  steps.push(thoughtStep);
626
689
  } else if (part.type === "tool_call") {
@@ -733,11 +796,7 @@ function parseInteraction(
733
796
  // truncated marker); absent args mean a zero-argument call.
734
797
  const rawArgs: unknown = step.arguments;
735
798
  const args =
736
- rawArgs && typeof rawArgs === "object"
737
- ? rawArgs
738
- : typeof rawArgs === "string"
739
- ? parseArguments(rawArgs)
740
- : {};
799
+ rawArgs && typeof rawArgs === "object" ? rawArgs : typeof rawArgs === "string" ? parseArguments(rawArgs) : {};
741
800
  toolCalls.push({
742
801
  id: step.id || newToolCallId(),
743
802
  name: step.name || "unknown",
@@ -815,12 +874,11 @@ export class GoogleInteractionsProvider implements Provider {
815
874
  // only signatures return and canonical `thinking` would always be empty.
816
875
  // Enabled whenever thinking is active — including the default/dynamic
817
876
  // case where no explicit level is sent — and off only when disabled.
818
- const thinkingActive =
819
- options?.thinking?.enabled !== false && (options?.thinking?.level ?? "dynamic") !== "none";
877
+ const thinkingActive = options?.thinking?.enabled !== false && (options?.thinking?.level ?? "dynamic") !== "none";
820
878
  if (thinkingActive) generationConfig["thinking_summaries"] = "auto";
821
879
  if (toolChoice) generationConfig["tool_choice"] = toolChoice;
822
- // max_output_tokens only; stop and seed warn+drop.
823
- Object.assign(generationConfig, mapGenerationParamsToGoogle(options, `google/${modelId}`));
880
+ // max_output_tokens only.
881
+ Object.assign(generationConfig, mapGenerationParamsToGoogle(options));
824
882
 
825
883
  const body: InteractionRequestBody = {
826
884
  model: modelId,
@@ -834,7 +892,9 @@ export class GoogleInteractionsProvider implements Provider {
834
892
  let outputEmulated = false;
835
893
  let outputSpec: import("../types/model.ts").StructuredOutputSpec | undefined;
836
894
  try {
837
- outputSpec = normalizeStructuredOutput(options?.output ?? (options as { outputSchema?: unknown })?.outputSchema);
895
+ outputSpec = normalizeStructuredOutput(
896
+ options?.output ?? (options as { outputSchema?: unknown })?.outputSchema
897
+ );
838
898
  } catch {
839
899
  outputSpec = undefined;
840
900
  }
@@ -846,7 +906,8 @@ export class GoogleInteractionsProvider implements Provider {
846
906
  provider: "google",
847
907
  capability: "structured output",
848
908
  requested: `${outputSpec.name} (google/${modelId})`,
849
- reason: "the catalog reports structured_output: false for this model, so native response_format is unsupported.",
909
+ reason:
910
+ "the catalog reports structured_output: false for this model, so native response_format is unsupported.",
850
911
  fallback: "emulated JSON instruction + client validation (no response_format is sent)",
851
912
  });
852
913
  }
@@ -927,7 +988,10 @@ export class GoogleInteractionsProvider implements Provider {
927
988
  apiKey: string,
928
989
  signal?: AbortSignal
929
990
  ): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
930
- const { signal: effective, cleanup } = combinedSignal(signal, timeoutFor(options as { timeoutMs?: number } | undefined));
991
+ const { signal: effective, cleanup } = combinedSignal(
992
+ signal,
993
+ timeoutFor(options as { timeoutMs?: number } | undefined)
994
+ );
931
995
  try {
932
996
  const res = await (options?.fetch ?? fetch)(url, this.requestInit(method, body, options, apiKey, effective));
933
997
  const text = await res.text();
@@ -944,7 +1008,6 @@ export class GoogleInteractionsProvider implements Provider {
944
1008
  // Throws a concise provider error (status, code, Retry-After, key hint) for a non-2xx reply; in: every Google HTTP call, out: nothing or a thrown error.
945
1009
  private throwIfError(
946
1010
  status: number,
947
- statusText: string,
948
1011
  url: string,
949
1012
  bodyText: string,
950
1013
  modelId: string,
@@ -989,9 +1052,14 @@ export class GoogleInteractionsProvider implements Provider {
989
1052
  modelId: string
990
1053
  ): Promise<InteractionReply> {
991
1054
  const res = await this.doFetch(url, method, body, options, apiKey, options?.signal);
992
- this.throwIfError(res.status, res.statusText, url, res.text, modelId, res.headers);
1055
+ this.throwIfError(res.status, url, res.text, modelId, res.headers);
993
1056
  try {
994
- return { status: res.status, statusText: res.statusText, headers: res.headers, interaction: JSON.parse(res.text) as InteractionObject };
1057
+ return {
1058
+ status: res.status,
1059
+ statusText: res.statusText,
1060
+ headers: res.headers,
1061
+ interaction: JSON.parse(res.text) as InteractionObject,
1062
+ };
995
1063
  } catch {
996
1064
  throw toConciseProviderError(
997
1065
  Object.assign(new Error("Invalid JSON response from Google Interactions API"), {
@@ -1006,7 +1074,12 @@ export class GoogleInteractionsProvider implements Provider {
1006
1074
  }
1007
1075
 
1008
1076
  // Fire-and-forget cancel so an aborted background run stops billing; in: an aborted generate/stream, out: POST {id}/cancel.
1009
- private cancelInteraction(baseUrl: string, id: string, options: ProviderRequestOptions | undefined, apiKey: string): void {
1077
+ private cancelInteraction(
1078
+ baseUrl: string,
1079
+ id: string,
1080
+ options: ProviderRequestOptions | undefined,
1081
+ apiKey: string
1082
+ ): void {
1010
1083
  void this.doFetch(`${baseUrl}/interactions/${id}/cancel`, "POST", undefined, options, apiKey).catch(() => {});
1011
1084
  }
1012
1085
 
@@ -1060,7 +1133,10 @@ export class GoogleInteractionsProvider implements Provider {
1060
1133
  try {
1061
1134
  for (let poll = 0; isRunning(reply.interaction.status); poll++) {
1062
1135
  await sleep(this.pollDelaysMs[poll === 0 ? 0 : 1], options?.signal);
1063
- reply = await withRetries(() => this.fetchInteraction(`${url}/${id}`, "GET", undefined, options, apiKey, clean), retryOptions);
1136
+ reply = await withRetries(
1137
+ () => this.fetchInteraction(`${url}/${id}`, "GET", undefined, options, apiKey, clean),
1138
+ retryOptions
1139
+ );
1064
1140
  }
1065
1141
  return reply;
1066
1142
  } catch (err) {
@@ -1071,7 +1147,10 @@ export class GoogleInteractionsProvider implements Provider {
1071
1147
 
1072
1148
  // Create (retried as a whole), wait if it runs in the background, then record the chain and map the result.
1073
1149
  const run = async (): Promise<ProviderGenerateResult> => {
1074
- let reply = await withRetries(() => this.fetchInteraction(url, "POST", body, options, apiKey, clean), retryOptions);
1150
+ let reply = await withRetries(
1151
+ () => this.fetchInteraction(url, "POST", body, options, apiKey, clean),
1152
+ retryOptions
1153
+ );
1075
1154
  if (body.background) reply = await waitForCompletion(reply);
1076
1155
  const { interaction } = reply;
1077
1156
  if (interaction.id && sessionId && body.store !== false) {
@@ -1081,12 +1160,10 @@ export class GoogleInteractionsProvider implements Provider {
1081
1160
  lastTurnTotals.delete(sessionId);
1082
1161
  }
1083
1162
  noteProviderTurn(sessionId, "google");
1084
- const parsed = parseInteraction(
1085
- interaction,
1086
- clean,
1087
- Date.now() - startTime,
1088
- { request: rawRequest, response: { status: reply.status, statusText: reply.statusText, headers: reply.headers, body: interaction } }
1089
- );
1163
+ const parsed = parseInteraction(interaction, clean, Date.now() - startTime, {
1164
+ request: rawRequest,
1165
+ response: { status: reply.status, statusText: reply.statusText, headers: reply.headers, body: interaction },
1166
+ });
1090
1167
  checkTurnTokens(sessionId, parsed.usage);
1091
1168
  return parsed;
1092
1169
  };
@@ -1165,9 +1242,16 @@ export class GoogleInteractionsProvider implements Provider {
1165
1242
  // (429/5xx/network) are retried before the first byte; a live stream is never retried.
1166
1243
  const open = (target: string, method: "POST" | "GET"): Promise<Response> =>
1167
1244
  openStreamResponse({
1168
- fetchOnce: () => withAbortTimer(linked, streamTimeoutMs, () => (options?.fetch ?? fetch)(target, this.requestInit(method, method === "POST" ? body : undefined, options, apiKey, linked.signal))),
1169
- onHttpError: (r, text) => this.throwIfError(r.status, r.statusText, target, text, clean, headersToRecord(r.headers)),
1170
- noBodyError: () => toConciseProviderError(new Error("Google streaming response had no body"), "google", clean),
1245
+ fetchOnce: () =>
1246
+ withAbortTimer(linked, streamTimeoutMs, () =>
1247
+ (options?.fetch ?? fetch)(
1248
+ target,
1249
+ this.requestInit(method, method === "POST" ? body : undefined, options, apiKey, linked.signal)
1250
+ )
1251
+ ),
1252
+ onHttpError: (r, text) => this.throwIfError(r.status, target, text, clean, headersToRecord(r.headers)),
1253
+ noBodyError: () =>
1254
+ toConciseProviderError(new Error("Google streaming response had no body"), "google", clean),
1171
1255
  retry: retryOptionsFrom(options),
1172
1256
  signal: linked.signal,
1173
1257
  label: { providerId: "google", modelId: clean },
@@ -1177,7 +1261,11 @@ export class GoogleInteractionsProvider implements Provider {
1177
1261
  res.headers.forEach((v, k) => {
1178
1262
  responseHeaders[k] = v;
1179
1263
  });
1180
- const responseMeta: Record<string, unknown> & { status: number; statusText: string; headers: Record<string, string> } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
1264
+ const responseMeta: Record<string, unknown> & {
1265
+ status: number;
1266
+ statusText: string;
1267
+ headers: Record<string, string>;
1268
+ } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
1181
1269
  eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
1182
1270
 
1183
1271
  let reader: StreamReader | undefined;
@@ -1286,7 +1374,8 @@ export class GoogleInteractionsProvider implements Provider {
1286
1374
  thinking += t;
1287
1375
  eventStream.push({ type: "thinking_delta", thinkingDelta: t, partialThinking: thinking });
1288
1376
  }
1289
- if (!signature && typeof step["signature"] === "string" && step["signature"]) signature = step["signature"];
1377
+ if (!signature && typeof step["signature"] === "string" && step["signature"])
1378
+ signature = step["signature"];
1290
1379
  } else if (step["type"] === "function_call") {
1291
1380
  const args = step["arguments"];
1292
1381
  const startArgs = typeof args === "string" ? args : args ? JSON.stringify(args) : "";
@@ -1343,7 +1432,8 @@ export class GoogleInteractionsProvider implements Provider {
1343
1432
  break;
1344
1433
  }
1345
1434
  if (chunk.done) break;
1346
- for (const m of parser.feed(decoder.decode(chunk.value, { stream: true }))) handleMessage(m.event, m.data);
1435
+ for (const m of parser.feed(decoder.decode(chunk.value, { stream: true })))
1436
+ handleMessage(m.event, m.data);
1347
1437
  }
1348
1438
  for (const m of parser.flush()) handleMessage(m.event, m.data);
1349
1439
  } finally {
@@ -1383,7 +1473,8 @@ export class GoogleInteractionsProvider implements Provider {
1383
1473
  }
1384
1474
 
1385
1475
  if (sessionId) {
1386
- if (responseId && body.store !== false) setChain(sessionId, { interactionId: responseId, model: clean, at: Date.now() });
1476
+ if (responseId && body.store !== false)
1477
+ setChain(sessionId, { interactionId: responseId, model: clean, at: Date.now() });
1387
1478
  else if (body.store === false) {
1388
1479
  interactionChains.delete(sessionId);
1389
1480
  lastTurnTotals.delete(sessionId);
@@ -1402,7 +1493,8 @@ export class GoogleInteractionsProvider implements Provider {
1402
1493
  const cleanThinkingEarly = thinking.replace(/\n{3,}/g, "\n\n").trim();
1403
1494
  const ctEntry = Object.entries(responseHeaders).find(([k]) => k.toLowerCase() === "content-type");
1404
1495
  const isSSEPayload =
1405
- (typeof ctEntry?.[1] === "string" && ((ctEntry[1] as string).includes("event-stream") || (ctEntry[1] as string).startsWith("text/"))) ||
1496
+ (typeof ctEntry?.[1] === "string" &&
1497
+ ((ctEntry[1] as string).includes("event-stream") || (ctEntry[1] as string).startsWith("text/"))) ||
1406
1498
  framesSeen > 0 ||
1407
1499
  badFrames > 0;
1408
1500
  if (!hasTerminal && !text && !cleanThinkingEarly && toolCalls.length === 0 && isSSEPayload) {
@@ -1433,14 +1525,10 @@ export class GoogleInteractionsProvider implements Provider {
1433
1525
  } catch (err: unknown) {
1434
1526
  const raw = err instanceof Error ? err : new Error(String(err));
1435
1527
  const isAbort =
1436
- linked.signal.aborted ||
1437
- eventStream.isCancelled() ||
1438
- (raw as { name?: string }).name === "AbortError";
1528
+ linked.signal.aborted || eventStream.isCancelled() || (raw as { name?: string }).name === "AbortError";
1439
1529
  if (isAbort) {
1440
1530
  cancelBackground();
1441
- eventStream.fail(
1442
- Object.assign(new Error("Stream aborted"), { name: "AbortError" })
1443
- );
1531
+ eventStream.fail(Object.assign(new Error("Stream aborted"), { name: "AbortError" }));
1444
1532
  } else if (evictSessionId && usedChain && isNotFoundChainError(raw)) {
1445
1533
  // Reactive self-heal, fail-clean variant (Rule 2): the `start`
1446
1534
  // event is already delivered, so a transparent in-stream retry
@@ -13,7 +13,7 @@
13
13
  * Strictness contract (differs from the old lenient passthrough on purpose):
14
14
  * - Requests carry ONLY standard Chat Completions fields (`model`, `messages`,
15
15
  * `tools`, `tool_choice`, `stream`, plus `max_tokens`/
16
- * `stop`/`seed` when explicitly set). No `reasoning`, `service_tier`,
16
+ * when explicitly set). No `reasoning`, `service_tier`,
17
17
  * `session_id`, `prompt_cache_key`, or other extras: strict endpoints
18
18
  * (groq, ollama, …) fail unknown body properties, so anything unsupported
19
19
  * is warn+drop, never sent.
@@ -66,7 +66,6 @@ import {
66
66
  newToolCallId,
67
67
  redactedHeaders,
68
68
  readErrorPayload,
69
- incompleteStreamError,
70
69
  hasUnfinishedToolCall,
71
70
  linkStreamAbort,
72
71
  withAbortTimer,
@@ -130,7 +129,10 @@ interface ChatRequestBody {
130
129
  messages: ChatMessage[];
131
130
  tools?: Array<Record<string, unknown>>;
132
131
  tool_choice?: string | { type: "function"; function: { name: string } };
133
- response_format?: { type: "json_schema"; json_schema: { name: string; schema: Record<string, unknown>; strict: boolean; description?: string } };
132
+ response_format?: {
133
+ type: "json_schema";
134
+ json_schema: { name: string; schema: Record<string, unknown>; strict: boolean; description?: string };
135
+ };
134
136
  stream?: boolean;
135
137
  [k: string]: unknown;
136
138
  }
@@ -231,18 +233,20 @@ function resolveApiKey(
231
233
  ): string | undefined {
232
234
  const clean = (v: unknown): string | undefined => {
233
235
  if (typeof v !== "string") return undefined;
234
- const t = v.trim().replace(/^["']|["']$/g, "").trim();
236
+ const t = v
237
+ .trim()
238
+ .replace(/^["']|["']$/g, "")
239
+ .trim();
235
240
  return t || undefined;
236
241
  };
237
242
  const envPrefix = envPrefixOf(prefix);
238
- const key = (
243
+ const key =
239
244
  clean(options?.apiKey) ||
240
245
  clean(configuredApiKey) ||
241
246
  clean(options?.env?.[`${envPrefix}_API_KEY`]) ||
242
247
  clean(options?.env?.[`${envPrefix}_BASE_API_KEY`]) ||
243
248
  getApiKey(prefix, undefined, options?.env) ||
244
- undefined
245
- );
249
+ undefined;
246
250
  if (key) return key;
247
251
  if (hasAuthHeader(options?.headers)) return "unused";
248
252
  return undefined;
@@ -308,7 +312,8 @@ function warnThinkingDropped(prefix: string, thinking: ThinkingConfig | undefine
308
312
  provider: prefix,
309
313
  capability: "thinking level",
310
314
  requested: `${level} (${modelRef})`,
311
- reason: "custom OpenAI-compatible endpoints define no portable reasoning control; levels vary by vendor and strict endpoints reject unknown fields.",
315
+ reason:
316
+ "custom OpenAI-compatible endpoints define no portable reasoning control; levels vary by vendor and strict endpoints reject unknown fields.",
312
317
  fallback: "the server default (no reasoning payload is sent)",
313
318
  });
314
319
  }
@@ -325,11 +330,7 @@ function audioFormatFor(mimeType?: string): string {
325
330
  * Completions subset has no shape for them, and a provider 400 would only say
326
331
  * so confusingly. Images and audio keep flowing natively.
327
332
  */
328
- function assertNoVideoOrFileParts(
329
- context: ProviderContext,
330
- prefix: string,
331
- modelId: string
332
- ): void {
333
+ function assertNoVideoOrFileParts(context: ProviderContext, prefix: string, modelId: string): void {
333
334
  let kind: "video" | "file" | undefined;
334
335
  for (const msg of context.messages) {
335
336
  if (!Array.isArray((msg as { content?: unknown }).content)) continue;
@@ -374,10 +375,7 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ChatContentPa
374
375
  const raw = (part as { audio?: unknown }).audio;
375
376
  const mimeType = (part as { mimeType?: string }).mimeType;
376
377
  // The OpenAI chat audio shape carries data only — always inline.
377
- const norm = await normalizeMediaInput(
378
- raw as string | Uint8Array | ArrayBuffer,
379
- mimeType
380
- );
378
+ const norm = await normalizeMediaInput(raw as string | Uint8Array | ArrayBuffer, mimeType);
381
379
  blocks.push({
382
380
  type: "input_audio",
383
381
  input_audio: { data: norm.base64Data, format: audioFormatFor(norm.mimeType) },
@@ -428,10 +426,16 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
428
426
  messages.push({ role: "assistant", content: "I have processed the tool results." });
429
427
  }
430
428
  if (typeof m.content === "string") {
431
- if (m.content) { messages.push({ role: "user", content: m.content }); lastRole = "user"; }
429
+ if (m.content) {
430
+ messages.push({ role: "user", content: m.content });
431
+ lastRole = "user";
432
+ }
432
433
  } else {
433
434
  const blocks = await contentPartsToBlocks(m.content);
434
- if (blocks.length > 0) { messages.push({ role: "user", content: blocks }); lastRole = "user"; }
435
+ if (blocks.length > 0) {
436
+ messages.push({ role: "user", content: blocks });
437
+ lastRole = "user";
438
+ }
435
439
  }
436
440
  } else if (m.role === "assistant") {
437
441
  if (typeof m.content === "string") {
@@ -527,25 +531,22 @@ function mapUsage(raw?: ChatResponse["usage"]): TokenUsage {
527
531
  }
528
532
 
529
533
  // The Gemini thought signature a proxy put on a tool call, if any; in: parseResponse, out: a string or undefined.
530
- function extractSignature(rawTc: {
531
- extra_content?: { google?: { thought_signature?: string } };
532
- }): string | undefined {
534
+ function extractSignature(rawTc: { extra_content?: { google?: { thought_signature?: string } } }): string | undefined {
533
535
  const sig = rawTc?.extra_content?.google?.thought_signature;
534
536
  return typeof sig === "string" && sig ? sig : undefined;
535
537
  }
536
538
 
537
539
  /** Tags a captured compat signature with its issuing provider (`google`). */
538
- function withCompatSignature(sig: string | undefined): { thoughtSignature?: string; thoughtSignatureProvider?: string } {
540
+ function withCompatSignature(sig: string | undefined): {
541
+ thoughtSignature?: string;
542
+ thoughtSignatureProvider?: string;
543
+ } {
539
544
  if (!sig) return {};
540
545
  return { thoughtSignature: sig, thoughtSignatureProvider: "google" };
541
546
  }
542
547
 
543
548
  // Throws a concise error for an HTTP 200 reply that carries only `error`; in: parseResponse, out: a thrown error.
544
- function throwResponseError(
545
- response: ChatResponse,
546
- prefix: string,
547
- modelId: string
548
- ): void {
549
+ function throwResponseError(response: ChatResponse, prefix: string, modelId: string): void {
549
550
  const message =
550
551
  response.error && typeof response.error.message === "string" && response.error.message
551
552
  ? response.error.message
@@ -716,7 +717,8 @@ export class OpenAICompatibleChatProvider implements Provider {
716
717
  provider: this.prefix,
717
718
  capability: "service tier",
718
719
  requested: `${options.serviceTier} (${this.prefix}/${modelId})`,
719
- reason: "custom OpenAI-compatible endpoints define no service-tier primitive, and strict endpoints reject unknown body properties.",
720
+ reason:
721
+ "custom OpenAI-compatible endpoints define no service-tier primitive, and strict endpoints reject unknown body properties.",
720
722
  fallback: "standard routing (no service_tier payload is sent)",
721
723
  });
722
724
  }
@@ -745,7 +747,8 @@ export class OpenAICompatibleChatProvider implements Provider {
745
747
  provider: this.prefix,
746
748
  capability: "structured output",
747
749
  requested: `${outputSpec.name} (${this.prefix}/${modelId})`,
748
- reason: "the catalog reports structured_output: false for this model, so native response_format is unsupported.",
750
+ reason:
751
+ "the catalog reports structured_output: false for this model, so native response_format is unsupported.",
749
752
  fallback: "emulated JSON instruction + client validation (no response_format is sent)",
750
753
  });
751
754
  }
@@ -758,7 +761,7 @@ export class OpenAICompatibleChatProvider implements Provider {
758
761
  const compatTools = toCompatTools(tools);
759
762
  if (compatTools) body.tools = compatTools;
760
763
  // Standard Chat Completions generation fields, only when the caller set them.
761
- Object.assign(body, mapGenerationParamsToChat(options, this.prefix, `${this.prefix}/${modelId}`));
764
+ Object.assign(body, mapGenerationParamsToChat(options));
762
765
  const toolChoice = mapToolChoiceToOpenAI(
763
766
  options?.toolChoice as "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
764
767
  );
@@ -791,7 +794,7 @@ export class OpenAICompatibleChatProvider implements Provider {
791
794
  const noStreamOptions =
792
795
  (options as { streamOptions?: unknown } | undefined)?.streamOptions === false ||
793
796
  getEnv("COMPAT_NO_STREAM_OPTIONS") === "1" ||
794
- (options?.env?.["COMPAT_NO_STREAM_OPTIONS"] === "1");
797
+ options?.env?.["COMPAT_NO_STREAM_OPTIONS"] === "1";
795
798
  if (!noStreamOptions) {
796
799
  (body as unknown as Record<string, unknown>)["stream_options"] = { include_usage: true };
797
800
  }
@@ -807,11 +810,7 @@ export class OpenAICompatibleChatProvider implements Provider {
807
810
  sessionId: string | undefined,
808
811
  signal?: AbortSignal
809
812
  ): RequestInit {
810
- const headers: Record<string, string> = buildSessionHeaders(
811
- this.prefix,
812
- options?.headers,
813
- sessionId
814
- );
813
+ const headers: Record<string, string> = buildSessionHeaders(this.prefix, options?.headers, sessionId);
815
814
  // Strict-endpoint opt-out (pi sendSessionAffinityHeaders pattern):
816
815
  // `COMPAT_NO_SESSION_HEADERS=1` or per-call `sendSessionHeaders:false`
817
816
  // drops affinity headers that trigger CORS preflights / strict 400s.
@@ -844,7 +843,10 @@ export class OpenAICompatibleChatProvider implements Provider {
844
843
  sessionId: string | undefined,
845
844
  signal?: AbortSignal
846
845
  ): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
847
- const { signal: effective, cleanup } = combinedSignal(signal, timeoutFor(options as { timeoutMs?: number } | undefined));
846
+ const { signal: effective, cleanup } = combinedSignal(
847
+ signal,
848
+ timeoutFor(options as { timeoutMs?: number } | undefined)
849
+ );
848
850
  try {
849
851
  const res = await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, effective));
850
852
  const text = await res.text();
@@ -939,13 +941,10 @@ export class OpenAICompatibleChatProvider implements Provider {
939
941
  // Stateless turns never establish chains, but the session store still
940
942
  // records the turn so other providers' switch detection keeps working.
941
943
  noteProviderTurn(sessionId, this.prefix);
942
- return parseResponse(
943
- response,
944
- this.prefix,
945
- clean,
946
- Date.now() - startTime,
947
- { request: rawRequest, response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response } }
948
- );
944
+ return parseResponse(response, this.prefix, clean, Date.now() - startTime, {
945
+ request: rawRequest,
946
+ response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response },
947
+ });
949
948
  };
950
949
 
951
950
  try {
@@ -1000,9 +999,13 @@ export class OpenAICompatibleChatProvider implements Provider {
1000
999
  // Transient failures (429/5xx/network) are retried here, before the
1001
1000
  // first byte; once the stream is live it is never retried.
1002
1001
  const res = await openStreamResponse({
1003
- fetchOnce: () => withAbortTimer(linked, streamTimeoutMs, () => (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))),
1002
+ fetchOnce: () =>
1003
+ withAbortTimer(linked, streamTimeoutMs, () =>
1004
+ (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))
1005
+ ),
1004
1006
  onHttpError: (r, text) => this.throwIfError(r.status, url, text, clean, headersToRecord(r.headers)),
1005
- noBodyError: () => toConciseProviderError(new Error("OpenAI-compatible streaming response had no body"), prefix, clean),
1007
+ noBodyError: () =>
1008
+ toConciseProviderError(new Error("OpenAI-compatible streaming response had no body"), prefix, clean),
1006
1009
  retry: retryOptionsFrom(options),
1007
1010
  signal: linked.signal,
1008
1011
  label: { providerId: prefix, modelId: clean },
@@ -1011,7 +1014,11 @@ export class OpenAICompatibleChatProvider implements Provider {
1011
1014
  res.headers.forEach((v, k) => {
1012
1015
  responseHeaders[k] = v;
1013
1016
  });
1014
- const responseMeta: Record<string, unknown> & { status: number; statusText: string; headers: Record<string, string> } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
1017
+ const responseMeta: Record<string, unknown> & {
1018
+ status: number;
1019
+ statusText: string;
1020
+ headers: Record<string, string>;
1021
+ } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
1015
1022
  eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
1016
1023
 
1017
1024
  let text = "";
@@ -1059,7 +1066,8 @@ export class OpenAICompatibleChatProvider implements Provider {
1059
1066
  if (typeof msg["id"] === "string" && !responseId) {
1060
1067
  responseId = msg["id"] as string;
1061
1068
  }
1062
- const choice = (Array.isArray(msg["choices"]) ? (msg["choices"] as Record<string, unknown>[])[0] : undefined) ?? {};
1069
+ const choice =
1070
+ (Array.isArray(msg["choices"]) ? (msg["choices"] as Record<string, unknown>[])[0] : undefined) ?? {};
1063
1071
  const delta = (choice["delta"] as Record<string, unknown>) ?? {};
1064
1072
  // Text delta (content-free accounting frames carry "" — ignored).
1065
1073
  // Refusal deltas surface as text with a `refusal` finish.