agent-accelerator 0.5.8 → 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 +26 -5
  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 +374 -167
  7. package/src/agent/architecture.md +14 -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 +449 -373
  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 +58 -28
  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 +46 -49
  23. package/src/providers/architecture.md +10 -8
  24. package/src/providers/google.ts +150 -62
  25. package/src/providers/openai-compat.ts +60 -58
  26. package/src/providers/openai.ts +57 -39
  27. package/src/providers/openrouter.ts +87 -32
  28. package/src/providers/registry.ts +12 -26
  29. package/src/providers/shared.ts +89 -39
  30. package/src/providers.ts +56 -115
  31. package/src/scheduler/architecture.md +9 -8
  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 +50 -44
  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 +17 -31
  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
@@ -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.
@@ -59,14 +59,13 @@ import { normalizeMediaInput } from "../utils/media.ts";
59
59
  import { safeStringify } from "../utils/serialization.ts";
60
60
  import { toConciseProviderError, assertModalitiesSupported } from "../utils/errors.ts";
61
61
  import { withRetries } from "../utils/retry.ts";
62
- import { ConfigError } from "../types/errors.ts";
62
+ import { ConfigError, ValidationError } from "../types/errors.ts";
63
63
  import {
64
64
  parseArguments,
65
65
  normalizeThinkingParts,
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;
@@ -350,13 +351,7 @@ function assertNoVideoOrFileParts(
350
351
  kind === "video"
351
352
  ? `[${prefix}/${modelId}] unsupported video input (generic OpenAI-compatible endpoints carry text/image/audio only). Use a video-capable model (e.g. google/gemini-*) or drop the video part.`
352
353
  : `[${prefix}/${modelId}] unsupported file input (generic OpenAI-compatible endpoints carry text/image/audio only). Use a file-capable model (e.g. google/gemini-*) or convert the document to text before sending it.`;
353
- const err = new Error(message);
354
- err.name = "AgentAccelProviderError";
355
- Object.defineProperties(err, {
356
- provider: { value: prefix, enumerable: false },
357
- model: { value: modelId, enumerable: false },
358
- });
359
- throw err;
354
+ throw new ValidationError(message, { provider: prefix, model: modelId });
360
355
  }
361
356
 
362
357
  // Text, image (link or data URL) and audio parts as Chat content blocks; in: a user message's parts, out: fullHistoryMessages.
@@ -380,10 +375,7 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ChatContentPa
380
375
  const raw = (part as { audio?: unknown }).audio;
381
376
  const mimeType = (part as { mimeType?: string }).mimeType;
382
377
  // The OpenAI chat audio shape carries data only — always inline.
383
- const norm = await normalizeMediaInput(
384
- raw as string | Uint8Array | ArrayBuffer,
385
- mimeType
386
- );
378
+ const norm = await normalizeMediaInput(raw as string | Uint8Array | ArrayBuffer, mimeType);
387
379
  blocks.push({
388
380
  type: "input_audio",
389
381
  input_audio: { data: norm.base64Data, format: audioFormatFor(norm.mimeType) },
@@ -434,10 +426,16 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
434
426
  messages.push({ role: "assistant", content: "I have processed the tool results." });
435
427
  }
436
428
  if (typeof m.content === "string") {
437
- 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
+ }
438
433
  } else {
439
434
  const blocks = await contentPartsToBlocks(m.content);
440
- 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
+ }
441
439
  }
442
440
  } else if (m.role === "assistant") {
443
441
  if (typeof m.content === "string") {
@@ -533,25 +531,22 @@ function mapUsage(raw?: ChatResponse["usage"]): TokenUsage {
533
531
  }
534
532
 
535
533
  // The Gemini thought signature a proxy put on a tool call, if any; in: parseResponse, out: a string or undefined.
536
- function extractSignature(rawTc: {
537
- extra_content?: { google?: { thought_signature?: string } };
538
- }): string | undefined {
534
+ function extractSignature(rawTc: { extra_content?: { google?: { thought_signature?: string } } }): string | undefined {
539
535
  const sig = rawTc?.extra_content?.google?.thought_signature;
540
536
  return typeof sig === "string" && sig ? sig : undefined;
541
537
  }
542
538
 
543
539
  /** Tags a captured compat signature with its issuing provider (`google`). */
544
- function withCompatSignature(sig: string | undefined): { thoughtSignature?: string; thoughtSignatureProvider?: string } {
540
+ function withCompatSignature(sig: string | undefined): {
541
+ thoughtSignature?: string;
542
+ thoughtSignatureProvider?: string;
543
+ } {
545
544
  if (!sig) return {};
546
545
  return { thoughtSignature: sig, thoughtSignatureProvider: "google" };
547
546
  }
548
547
 
549
548
  // Throws a concise error for an HTTP 200 reply that carries only `error`; in: parseResponse, out: a thrown error.
550
- function throwResponseError(
551
- response: ChatResponse,
552
- prefix: string,
553
- modelId: string
554
- ): void {
549
+ function throwResponseError(response: ChatResponse, prefix: string, modelId: string): void {
555
550
  const message =
556
551
  response.error && typeof response.error.message === "string" && response.error.message
557
552
  ? response.error.message
@@ -701,7 +696,7 @@ export class OpenAICompatibleChatProvider implements Provider {
701
696
  private requireApiKey(baseUrl: string, options?: ProviderRequestOptions): string | undefined {
702
697
  const key = this.apiKey(options);
703
698
  if (!key && !isLocalEndpoint(baseUrl)) {
704
- throw new Error(
699
+ throw new ConfigError(
705
700
  `[Agent Accelerator] Missing API key for "${this.prefix}". Set ${envPrefixOf(this.prefix)}_API_KEY or pass apiKey. (Local endpoints may omit the key.)`
706
701
  );
707
702
  }
@@ -722,7 +717,8 @@ export class OpenAICompatibleChatProvider implements Provider {
722
717
  provider: this.prefix,
723
718
  capability: "service tier",
724
719
  requested: `${options.serviceTier} (${this.prefix}/${modelId})`,
725
- 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.",
726
722
  fallback: "standard routing (no service_tier payload is sent)",
727
723
  });
728
724
  }
@@ -751,7 +747,8 @@ export class OpenAICompatibleChatProvider implements Provider {
751
747
  provider: this.prefix,
752
748
  capability: "structured output",
753
749
  requested: `${outputSpec.name} (${this.prefix}/${modelId})`,
754
- 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.",
755
752
  fallback: "emulated JSON instruction + client validation (no response_format is sent)",
756
753
  });
757
754
  }
@@ -764,7 +761,7 @@ export class OpenAICompatibleChatProvider implements Provider {
764
761
  const compatTools = toCompatTools(tools);
765
762
  if (compatTools) body.tools = compatTools;
766
763
  // Standard Chat Completions generation fields, only when the caller set them.
767
- Object.assign(body, mapGenerationParamsToChat(options, this.prefix, `${this.prefix}/${modelId}`));
764
+ Object.assign(body, mapGenerationParamsToChat(options));
768
765
  const toolChoice = mapToolChoiceToOpenAI(
769
766
  options?.toolChoice as "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
770
767
  );
@@ -797,7 +794,7 @@ export class OpenAICompatibleChatProvider implements Provider {
797
794
  const noStreamOptions =
798
795
  (options as { streamOptions?: unknown } | undefined)?.streamOptions === false ||
799
796
  getEnv("COMPAT_NO_STREAM_OPTIONS") === "1" ||
800
- (options?.env?.["COMPAT_NO_STREAM_OPTIONS"] === "1");
797
+ options?.env?.["COMPAT_NO_STREAM_OPTIONS"] === "1";
801
798
  if (!noStreamOptions) {
802
799
  (body as unknown as Record<string, unknown>)["stream_options"] = { include_usage: true };
803
800
  }
@@ -813,11 +810,7 @@ export class OpenAICompatibleChatProvider implements Provider {
813
810
  sessionId: string | undefined,
814
811
  signal?: AbortSignal
815
812
  ): RequestInit {
816
- const headers: Record<string, string> = buildSessionHeaders(
817
- this.prefix,
818
- options?.headers,
819
- sessionId
820
- );
813
+ const headers: Record<string, string> = buildSessionHeaders(this.prefix, options?.headers, sessionId);
821
814
  // Strict-endpoint opt-out (pi sendSessionAffinityHeaders pattern):
822
815
  // `COMPAT_NO_SESSION_HEADERS=1` or per-call `sendSessionHeaders:false`
823
816
  // drops affinity headers that trigger CORS preflights / strict 400s.
@@ -850,7 +843,10 @@ export class OpenAICompatibleChatProvider implements Provider {
850
843
  sessionId: string | undefined,
851
844
  signal?: AbortSignal
852
845
  ): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
853
- 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
+ );
854
850
  try {
855
851
  const res = await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, effective));
856
852
  const text = await res.text();
@@ -945,13 +941,10 @@ export class OpenAICompatibleChatProvider implements Provider {
945
941
  // Stateless turns never establish chains, but the session store still
946
942
  // records the turn so other providers' switch detection keeps working.
947
943
  noteProviderTurn(sessionId, this.prefix);
948
- return parseResponse(
949
- response,
950
- this.prefix,
951
- clean,
952
- Date.now() - startTime,
953
- { request: rawRequest, response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response } }
954
- );
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
+ });
955
948
  };
956
949
 
957
950
  try {
@@ -1006,9 +999,13 @@ export class OpenAICompatibleChatProvider implements Provider {
1006
999
  // Transient failures (429/5xx/network) are retried here, before the
1007
1000
  // first byte; once the stream is live it is never retried.
1008
1001
  const res = await openStreamResponse({
1009
- 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
+ ),
1010
1006
  onHttpError: (r, text) => this.throwIfError(r.status, url, text, clean, headersToRecord(r.headers)),
1011
- 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),
1012
1009
  retry: retryOptionsFrom(options),
1013
1010
  signal: linked.signal,
1014
1011
  label: { providerId: prefix, modelId: clean },
@@ -1017,7 +1014,11 @@ export class OpenAICompatibleChatProvider implements Provider {
1017
1014
  res.headers.forEach((v, k) => {
1018
1015
  responseHeaders[k] = v;
1019
1016
  });
1020
- 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 };
1021
1022
  eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
1022
1023
 
1023
1024
  let text = "";
@@ -1065,7 +1066,8 @@ export class OpenAICompatibleChatProvider implements Provider {
1065
1066
  if (typeof msg["id"] === "string" && !responseId) {
1066
1067
  responseId = msg["id"] as string;
1067
1068
  }
1068
- 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) ?? {};
1069
1071
  const delta = (choice["delta"] as Record<string, unknown>) ?? {};
1070
1072
  // Text delta (content-free accounting frames carry "" — ignored).
1071
1073
  // Refusal deltas surface as text with a `refusal` finish.
@@ -12,7 +12,7 @@
12
12
  * effort, service_tier flex/priority, prompt_cache_key affinity, usage,
13
13
  * finish reasons, switching, abort, errors.
14
14
  *
15
- * Also implemented: `text.format` structured output, `max_output_tokens` (stop + seed have no Responses equivalent: warn+drop).
15
+ * Also implemented: `text.format` structured output, `max_output_tokens`.
16
16
  *
17
17
  * Out of scope (never sent): background, conversation, previous_response_id
18
18
  * chaining, include, metadata, truncation, safety_identifier,
@@ -51,6 +51,7 @@ import { clampCacheKey } from "../utils/cache.ts";
51
51
  import { normalizeMediaInput } from "../utils/media.ts";
52
52
  import { safeStringify } from "../utils/serialization.ts";
53
53
  import { toConciseProviderError, assertModalitiesSupported, assertNoVideoPartsOnResponses } from "../utils/errors.ts";
54
+ import { ConfigError } from "../types/errors.ts";
54
55
  import { withRetries } from "../utils/retry.ts";
55
56
  import {
56
57
  parseArguments,
@@ -58,7 +59,6 @@ import {
58
59
  newToolCallId,
59
60
  redactedHeaders,
60
61
  readErrorPayload,
61
- incompleteStreamError,
62
62
  hasUnfinishedToolCall,
63
63
  linkStreamAbort,
64
64
  withAbortTimer,
@@ -70,7 +70,6 @@ import {
70
70
  combinedSignal,
71
71
  timeoutFor,
72
72
  hasAuthHeader,
73
- clampToolCallId,
74
73
  openStreamResponse,
75
74
  retryOptionsFrom,
76
75
  headersToRecord,
@@ -123,7 +122,15 @@ interface ResponsesRequestBody {
123
122
  prompt_cache_key?: string;
124
123
  store?: boolean;
125
124
  stream?: boolean;
126
- text?: { format: { type: "json_schema"; name: string; schema: Record<string, unknown>; strict: boolean; description?: string } };
125
+ text?: {
126
+ format: {
127
+ type: "json_schema";
128
+ name: string;
129
+ schema: Record<string, unknown>;
130
+ strict: boolean;
131
+ description?: string;
132
+ };
133
+ };
127
134
  [k: string]: unknown;
128
135
  }
129
136
 
@@ -211,13 +218,9 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ResponseConte
211
218
  for (const part of parts) {
212
219
  if (part.type === "text" && part.text) {
213
220
  blocks.push({ type: "input_text", text: part.text });
214
- } else if (
215
- part.type === "image" ||
216
- part.type === "audio" ||
217
- part.type === "video" ||
218
- part.type === "file"
219
- ) {
220
- const raw = (part as { image?: unknown; audio?: unknown; video?: unknown; file?: unknown }).image ??
221
+ } else if (part.type === "image" || part.type === "audio" || part.type === "video" || part.type === "file") {
222
+ const raw =
223
+ (part as { image?: unknown; audio?: unknown; video?: unknown; file?: unknown }).image ??
221
224
  (part as { audio?: unknown }).audio ??
222
225
  (part as { video?: unknown }).video ??
223
226
  (part as { file?: unknown }).file;
@@ -318,7 +321,8 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
318
321
  if (m.role === "system") continue;
319
322
  if (m.role === "user") {
320
323
  if (typeof m.content === "string") {
321
- if (m.content) items.push({ type: "message", role: "user", content: [{ type: "input_text", text: m.content }] });
324
+ if (m.content)
325
+ items.push({ type: "message", role: "user", content: [{ type: "input_text", text: m.content }] });
322
326
  } else {
323
327
  const blocks = await contentPartsToBlocks(m.content);
324
328
  if (blocks.length > 0) items.push({ type: "message", role: "user", content: blocks });
@@ -362,7 +366,11 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
362
366
  // provider-minted ids that canonical history does not retain.
363
367
  }
364
368
  if (texts.length > 0) {
365
- items.push({ type: "message", role: "assistant", content: texts.map((t) => ({ type: "output_text", text: t })) });
369
+ items.push({
370
+ type: "message",
371
+ role: "assistant",
372
+ content: texts.map((t) => ({ type: "output_text", text: t })),
373
+ });
366
374
  }
367
375
  } else if (m.role === "tool") {
368
376
  if (typeof m.content === "string") {
@@ -584,8 +592,8 @@ export class OpenAIResponsesProvider implements Provider {
584
592
  if (toolChoice !== undefined) body.tool_choice = toolChoice;
585
593
  if (effort) body.reasoning = { effort };
586
594
  if (serviceTier) body.service_tier = serviceTier;
587
- // max_output_tokens (stop + seed warn+drop: not on this wire).
588
- Object.assign(body, mapGenerationParamsToOpenAI(options, `openai/${modelId}`));
595
+ // max_output_tokens.
596
+ Object.assign(body, mapGenerationParamsToOpenAI(options));
589
597
  // Structured outputs: Responses `text.format{type:json_schema,name,schema,strict}`.
590
598
  // Skipped when emulated (instruction already in `instructions`).
591
599
  if (outputSpec && !outputEmulated) {
@@ -639,11 +647,7 @@ export class OpenAIResponsesProvider implements Provider {
639
647
  sessionId: string | undefined,
640
648
  signal?: AbortSignal
641
649
  ): RequestInit {
642
- const headers: Record<string, string> = buildSessionHeaders(
643
- "openai",
644
- options?.headers,
645
- sessionId
646
- );
650
+ const headers: Record<string, string> = buildSessionHeaders("openai", options?.headers, sessionId);
647
651
  // Case-insensitive merge so a lowercase `authorization` custom header
648
652
  // never coexists with the canonical bearer. Proxy flows
649
653
  // (`apiKey === "unused"`) preserve custom auth instead of overwriting.
@@ -661,7 +665,10 @@ export class OpenAIResponsesProvider implements Provider {
661
665
  sessionId: string | undefined,
662
666
  signal?: AbortSignal
663
667
  ): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
664
- const { signal: effective, cleanup } = combinedSignal(signal, timeoutFor(options as { timeoutMs?: number } | undefined));
668
+ const { signal: effective, cleanup } = combinedSignal(
669
+ signal,
670
+ timeoutFor(options as { timeoutMs?: number } | undefined)
671
+ );
665
672
  try {
666
673
  const res = await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, effective));
667
674
  const text = await res.text();
@@ -716,7 +723,7 @@ export class OpenAIResponsesProvider implements Provider {
716
723
  const clean = cleanModelId(model);
717
724
  const apiKey = resolveApiKey(options);
718
725
  if (!apiKey) {
719
- throw new Error(
726
+ throw new ConfigError(
720
727
  "[Agent Accelerator] Missing OpenAI API key. Set OPENAI_API_KEY (or OPENAI_BASE_API_KEY) or pass apiKey."
721
728
  );
722
729
  }
@@ -768,12 +775,10 @@ export class OpenAIResponsesProvider implements Provider {
768
775
  // Stateless turns never establish chains, but the session store still
769
776
  // records the turn so other providers' switch detection keeps working.
770
777
  noteProviderTurn(sessionId, "openai");
771
- return parseResponse(
772
- response,
773
- clean,
774
- Date.now() - startTime,
775
- { request: rawRequest, response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response } }
776
- );
778
+ return parseResponse(response, clean, Date.now() - startTime, {
779
+ request: rawRequest,
780
+ response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response },
781
+ });
777
782
  };
778
783
 
779
784
  try {
@@ -804,7 +809,7 @@ export class OpenAIResponsesProvider implements Provider {
804
809
  try {
805
810
  const apiKey = resolveApiKey(options);
806
811
  if (!apiKey) {
807
- throw new Error(
812
+ throw new ConfigError(
808
813
  "[Agent Accelerator] Missing OpenAI API key. Set OPENAI_API_KEY (or OPENAI_BASE_API_KEY) or pass apiKey."
809
814
  );
810
815
  }
@@ -833,9 +838,13 @@ export class OpenAIResponsesProvider implements Provider {
833
838
  // Transient failures (429/5xx/network) are retried here, before the
834
839
  // first byte; once the stream is live it is never retried.
835
840
  const res = await openStreamResponse({
836
- fetchOnce: () => withAbortTimer(linked, streamTimeoutMs, () => (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))),
841
+ fetchOnce: () =>
842
+ withAbortTimer(linked, streamTimeoutMs, () =>
843
+ (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))
844
+ ),
837
845
  onHttpError: (r, text) => this.throwIfError(r.status, url, text, clean, headersToRecord(r.headers)),
838
- noBodyError: () => toConciseProviderError(new Error("OpenAI streaming response had no body"), "openai", clean),
846
+ noBodyError: () =>
847
+ toConciseProviderError(new Error("OpenAI streaming response had no body"), "openai", clean),
839
848
  retry: retryOptionsFrom(options),
840
849
  signal: linked.signal,
841
850
  label: { providerId: "openai", modelId: clean },
@@ -844,13 +853,20 @@ export class OpenAIResponsesProvider implements Provider {
844
853
  res.headers.forEach((v, k) => {
845
854
  responseHeaders[k] = v;
846
855
  });
847
- const responseMeta: Record<string, unknown> & { status: number; statusText: string; headers: Record<string, string> } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
856
+ const responseMeta: Record<string, unknown> & {
857
+ status: number;
858
+ statusText: string;
859
+ headers: Record<string, string>;
860
+ } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
848
861
  eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
849
862
 
850
863
  let text = "";
851
864
  let thinking = "";
852
865
  // Tool calls keyed by output_index: { itemId, callId, name, startArgs, deltaArgs }.
853
- const calls = new Map<number, { itemId: string; callId: string; name: string; startArgs: string; deltaArgs: string }>();
866
+ const calls = new Map<
867
+ number,
868
+ { itemId: string; callId: string; name: string; startArgs: string; deltaArgs: string }
869
+ >();
854
870
  let usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
855
871
  let finishReason = "stop";
856
872
  let sawFinishReason = false;
@@ -891,10 +907,7 @@ export class OpenAIResponsesProvider implements Provider {
891
907
  } else if (type === "response.output_text.delta" && typeof msg["delta"] === "string") {
892
908
  text += msg["delta"] as string;
893
909
  eventStream.push({ type: "text_delta", delta: msg["delta"] as string, partialText: text });
894
- } else if (
895
- type === "response.refusal.delta" ||
896
- type === "response.output_refusal.delta"
897
- ) {
910
+ } else if (type === "response.refusal.delta" || type === "response.output_refusal.delta") {
898
911
  // Refusal deltas carry the filter text; surface as text and
899
912
  // remember for a `refusal` finish reason.
900
913
  if (typeof msg["delta"] === "string" && msg["delta"]) {
@@ -907,7 +920,11 @@ export class OpenAIResponsesProvider implements Provider {
907
920
  typeof msg["delta"] === "string"
908
921
  ) {
909
922
  thinking += msg["delta"] as string;
910
- eventStream.push({ type: "thinking_delta", thinkingDelta: msg["delta"] as string, partialThinking: thinking });
923
+ eventStream.push({
924
+ type: "thinking_delta",
925
+ thinkingDelta: msg["delta"] as string,
926
+ partialThinking: thinking,
927
+ });
911
928
  } else if (type === "response.output_item.added") {
912
929
  const index = (msg["output_index"] as number) ?? 0;
913
930
  const item = (msg["item"] as Record<string, unknown>) ?? {};
@@ -956,7 +973,8 @@ export class OpenAIResponsesProvider implements Provider {
956
973
  // Non-streaming maps `incomplete` → `length` (max tokens). Streaming
957
974
  // must do the same — otherwise truncated turns misreport `stop`.
958
975
  // Content-filter incompletes surface as `refusal`.
959
- const incompleteReason = (response as { incomplete_details?: { reason?: string } }).incomplete_details?.reason;
976
+ const incompleteReason = (response as { incomplete_details?: { reason?: string } }).incomplete_details
977
+ ?.reason;
960
978
  if (response.status === "incomplete") {
961
979
  finishReason = incompleteReason === "content_filter" ? "refusal" : "length";
962
980
  sawFinishReason = true;