agent-accelerator 0.5.9 → 0.6.5

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 (85) hide show
  1. package/README.md +64 -16
  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 +447 -172
  7. package/src/agent/architecture.md +16 -12
  8. package/src/agent/config-blocks.ts +39 -12
  9. package/src/agent/context.ts +9 -3
  10. package/src/agent/defaults.ts +88 -0
  11. package/src/agent/delegation.ts +86 -28
  12. package/src/agent/loop.ts +474 -374
  13. package/src/agent/subagent.ts +1 -3
  14. package/src/agent/subagents-tool.ts +50 -19
  15. package/src/agent/turns.ts +3 -1
  16. package/src/agent/usage-limit.ts +92 -0
  17. package/src/agent/worker.ts +73 -30
  18. package/src/agent/workers.ts +126 -21
  19. package/src/architecture.md +17 -15
  20. package/src/index.ts +43 -19
  21. package/src/models/architecture.md +4 -3
  22. package/src/models/catalog-cache.ts +6 -5
  23. package/src/models/catalog.ts +39 -30
  24. package/src/prompts/additional-context.md +2 -0
  25. package/src/prompts/architecture.md +19 -0
  26. package/src/prompts/prompts.ts +81 -0
  27. package/src/prompts/reasoning-directive.md +5 -0
  28. package/src/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +4 -0
  29. package/src/prompts/structured-output.md +2 -0
  30. package/src/{agent/subagents.md → prompts/subagents-tool.md} +3 -0
  31. package/src/prompts/tool-bridge.md +1 -0
  32. package/src/prompts/tool-interrupted.md +1 -0
  33. package/src/prompts/tool-not-found.md +1 -0
  34. package/src/prompts/tool-repeat-blocked.md +1 -0
  35. package/src/prompts/tool-schema-error.md +1 -0
  36. package/src/prompts/tool-truncated-arguments.md +1 -0
  37. package/src/prompts/worker-failed.md +1 -0
  38. package/src/prompts/worker-replied.md +2 -0
  39. package/src/providers/architecture.md +10 -8
  40. package/src/providers/google.ts +228 -69
  41. package/src/providers/openai-compat.ts +74 -56
  42. package/src/providers/openai.ts +64 -41
  43. package/src/providers/openrouter.ts +270 -48
  44. package/src/providers/registry.ts +6 -21
  45. package/src/providers/shared.ts +87 -33
  46. package/src/providers.ts +115 -124
  47. package/src/scheduler/architecture.md +8 -7
  48. package/src/scheduler/scheduler.ts +226 -60
  49. package/src/scheduler/store.ts +23 -10
  50. package/src/scheduler/time.ts +17 -3
  51. package/src/scheduler/tool.ts +38 -16
  52. package/src/session/architecture.md +7 -6
  53. package/src/session/journal.ts +39 -12
  54. package/src/session/store.ts +126 -61
  55. package/src/session/usage.ts +58 -9
  56. package/src/streaming/architecture.md +4 -3
  57. package/src/streaming/event-stream.ts +26 -12
  58. package/src/telemetry.ts +6 -2
  59. package/src/tools/architecture.md +9 -7
  60. package/src/tools/executor.ts +109 -62
  61. package/src/tools/schema.ts +10 -6
  62. package/src/tools/subagent-result.ts +6 -1
  63. package/src/tools/tool-content.ts +32 -0
  64. package/src/tools/tool.ts +9 -11
  65. package/src/types/agent.ts +37 -22
  66. package/src/types/architecture.md +11 -10
  67. package/src/types/core.ts +5 -9
  68. package/src/types/errors.ts +26 -4
  69. package/src/types/message.ts +7 -0
  70. package/src/types/model.ts +4 -5
  71. package/src/types/scheduler.ts +4 -4
  72. package/src/types/tool.ts +14 -2
  73. package/src/update-models.ts +5 -5
  74. package/src/utils/architecture.md +16 -15
  75. package/src/utils/cache.ts +1 -3
  76. package/src/utils/durable.ts +17 -5
  77. package/src/utils/env.ts +9 -7
  78. package/src/utils/errors.ts +9 -17
  79. package/src/utils/headers.ts +3 -10
  80. package/src/utils/media.ts +23 -8
  81. package/src/utils/raw-capture.ts +9 -2
  82. package/src/utils/retry.ts +15 -3
  83. package/src/utils/safe-io.ts +29 -7
  84. package/src/utils/serialization.ts +1 -4
  85. 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.
@@ -60,13 +60,13 @@ import { safeStringify } from "../utils/serialization.ts";
60
60
  import { toConciseProviderError, assertModalitiesSupported } from "../utils/errors.ts";
61
61
  import { withRetries } from "../utils/retry.ts";
62
62
  import { ConfigError, ValidationError } from "../types/errors.ts";
63
+ import { DEFAULT_PROMPTS } from "../prompts/prompts.ts";
63
64
  import {
64
65
  parseArguments,
65
66
  normalizeThinkingParts,
66
67
  newToolCallId,
67
68
  redactedHeaders,
68
69
  readErrorPayload,
69
- incompleteStreamError,
70
70
  hasUnfinishedToolCall,
71
71
  linkStreamAbort,
72
72
  withAbortTimer,
@@ -97,6 +97,8 @@ import {
97
97
  normalizeStructuredOutput,
98
98
  augmentSystemPromptWithStructuredOutput,
99
99
  warnIgnoredMediaResolution,
100
+ warnIgnoredVideoProcessing,
101
+ warnIgnoredToolMedia,
100
102
  } from "../providers.ts";
101
103
 
102
104
  // ---------------------------------------------------------------------------
@@ -130,7 +132,10 @@ interface ChatRequestBody {
130
132
  messages: ChatMessage[];
131
133
  tools?: Array<Record<string, unknown>>;
132
134
  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 } };
135
+ response_format?: {
136
+ type: "json_schema";
137
+ json_schema: { name: string; schema: Record<string, unknown>; strict: boolean; description?: string };
138
+ };
134
139
  stream?: boolean;
135
140
  [k: string]: unknown;
136
141
  }
@@ -231,18 +236,20 @@ function resolveApiKey(
231
236
  ): string | undefined {
232
237
  const clean = (v: unknown): string | undefined => {
233
238
  if (typeof v !== "string") return undefined;
234
- const t = v.trim().replace(/^["']|["']$/g, "").trim();
239
+ const t = v
240
+ .trim()
241
+ .replace(/^["']|["']$/g, "")
242
+ .trim();
235
243
  return t || undefined;
236
244
  };
237
245
  const envPrefix = envPrefixOf(prefix);
238
- const key = (
246
+ const key =
239
247
  clean(options?.apiKey) ||
240
248
  clean(configuredApiKey) ||
241
249
  clean(options?.env?.[`${envPrefix}_API_KEY`]) ||
242
250
  clean(options?.env?.[`${envPrefix}_BASE_API_KEY`]) ||
243
251
  getApiKey(prefix, undefined, options?.env) ||
244
- undefined
245
- );
252
+ undefined;
246
253
  if (key) return key;
247
254
  if (hasAuthHeader(options?.headers)) return "unused";
248
255
  return undefined;
@@ -308,7 +315,8 @@ function warnThinkingDropped(prefix: string, thinking: ThinkingConfig | undefine
308
315
  provider: prefix,
309
316
  capability: "thinking level",
310
317
  requested: `${level} (${modelRef})`,
311
- reason: "custom OpenAI-compatible endpoints define no portable reasoning control; levels vary by vendor and strict endpoints reject unknown fields.",
318
+ reason:
319
+ "custom OpenAI-compatible endpoints define no portable reasoning control; levels vary by vendor and strict endpoints reject unknown fields.",
312
320
  fallback: "the server default (no reasoning payload is sent)",
313
321
  });
314
322
  }
@@ -325,11 +333,7 @@ function audioFormatFor(mimeType?: string): string {
325
333
  * Completions subset has no shape for them, and a provider 400 would only say
326
334
  * so confusingly. Images and audio keep flowing natively.
327
335
  */
328
- function assertNoVideoOrFileParts(
329
- context: ProviderContext,
330
- prefix: string,
331
- modelId: string
332
- ): void {
336
+ function assertNoVideoOrFileParts(context: ProviderContext, prefix: string, modelId: string): void {
333
337
  let kind: "video" | "file" | undefined;
334
338
  for (const msg of context.messages) {
335
339
  if (!Array.isArray((msg as { content?: unknown }).content)) continue;
@@ -374,10 +378,7 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ChatContentPa
374
378
  const raw = (part as { audio?: unknown }).audio;
375
379
  const mimeType = (part as { mimeType?: string }).mimeType;
376
380
  // The OpenAI chat audio shape carries data only — always inline.
377
- const norm = await normalizeMediaInput(
378
- raw as string | Uint8Array | ArrayBuffer,
379
- mimeType
380
- );
381
+ const norm = await normalizeMediaInput(raw as string | Uint8Array | ArrayBuffer, mimeType);
381
382
  blocks.push({
382
383
  type: "input_audio",
383
384
  input_audio: { data: norm.base64Data, format: audioFormatFor(norm.mimeType) },
@@ -404,7 +405,10 @@ function resultToOutput(result: unknown): string {
404
405
  * `google` (or untagged legacy). Signatures minted by other providers never
405
406
  * leak onto this transport.
406
407
  */
407
- async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessage[]> {
408
+ async function fullHistoryMessages(
409
+ context: ProviderContext,
410
+ bridge = DEFAULT_PROMPTS.toolBridge
411
+ ): Promise<ChatMessage[]> {
408
412
  const pairing = new Map<string, string>();
409
413
  for (const m of context.messages) {
410
414
  if (m.role === "assistant" && Array.isArray(m.content)) {
@@ -425,13 +429,19 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
425
429
  if (m.role === "system") continue;
426
430
  if (m.role === "user") {
427
431
  if (lastRole === "tool") {
428
- messages.push({ role: "assistant", content: "I have processed the tool results." });
432
+ messages.push({ role: "assistant", content: bridge });
429
433
  }
430
434
  if (typeof m.content === "string") {
431
- if (m.content) { messages.push({ role: "user", content: m.content }); lastRole = "user"; }
435
+ if (m.content) {
436
+ messages.push({ role: "user", content: m.content });
437
+ lastRole = "user";
438
+ }
432
439
  } else {
433
440
  const blocks = await contentPartsToBlocks(m.content);
434
- if (blocks.length > 0) { messages.push({ role: "user", content: blocks }); lastRole = "user"; }
441
+ if (blocks.length > 0) {
442
+ messages.push({ role: "user", content: blocks });
443
+ lastRole = "user";
444
+ }
435
445
  }
436
446
  } else if (m.role === "assistant") {
437
447
  if (typeof m.content === "string") {
@@ -527,25 +537,22 @@ function mapUsage(raw?: ChatResponse["usage"]): TokenUsage {
527
537
  }
528
538
 
529
539
  // 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 {
540
+ function extractSignature(rawTc: { extra_content?: { google?: { thought_signature?: string } } }): string | undefined {
533
541
  const sig = rawTc?.extra_content?.google?.thought_signature;
534
542
  return typeof sig === "string" && sig ? sig : undefined;
535
543
  }
536
544
 
537
545
  /** Tags a captured compat signature with its issuing provider (`google`). */
538
- function withCompatSignature(sig: string | undefined): { thoughtSignature?: string; thoughtSignatureProvider?: string } {
546
+ function withCompatSignature(sig: string | undefined): {
547
+ thoughtSignature?: string;
548
+ thoughtSignatureProvider?: string;
549
+ } {
539
550
  if (!sig) return {};
540
551
  return { thoughtSignature: sig, thoughtSignatureProvider: "google" };
541
552
  }
542
553
 
543
554
  // 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 {
555
+ function throwResponseError(response: ChatResponse, prefix: string, modelId: string): void {
549
556
  const message =
550
557
  response.error && typeof response.error.message === "string" && response.error.message
551
558
  ? response.error.message
@@ -716,13 +723,16 @@ export class OpenAICompatibleChatProvider implements Provider {
716
723
  provider: this.prefix,
717
724
  capability: "service tier",
718
725
  requested: `${options.serviceTier} (${this.prefix}/${modelId})`,
719
- reason: "custom OpenAI-compatible endpoints define no service-tier primitive, and strict endpoints reject unknown body properties.",
726
+ reason:
727
+ "custom OpenAI-compatible endpoints define no service-tier primitive, and strict endpoints reject unknown body properties.",
720
728
  fallback: "standard routing (no service_tier payload is sent)",
721
729
  });
722
730
  }
723
731
  // Strict endpoints reject unknown body fields: implicit caching only.
724
732
  resolveCache(this.prefix, options?.cache, ["implicit"], `${this.prefix}/${modelId}`);
725
733
  warnIgnoredMediaResolution(this.prefix, context.messages, `${this.prefix}/${modelId}`);
734
+ warnIgnoredVideoProcessing(this.prefix, context.messages, `${this.prefix}/${modelId}`);
735
+ warnIgnoredToolMedia(this.prefix, context.messages, `${this.prefix}/${modelId}`);
726
736
 
727
737
  // Structured outputs: native `response_format` when supported, else
728
738
  // emulated via system prompt (strict endpoints would 400 on json_schema
@@ -739,17 +749,22 @@ export class OpenAICompatibleChatProvider implements Provider {
739
749
  outputEmulated = true;
740
750
  effectiveContext = {
741
751
  ...context,
742
- systemPrompt: augmentSystemPromptWithStructuredOutput(context.systemPrompt, outputSpec),
752
+ systemPrompt: augmentSystemPromptWithStructuredOutput(
753
+ context.systemPrompt,
754
+ outputSpec,
755
+ options?.prompts?.structuredOutput
756
+ ),
743
757
  };
744
758
  emitProviderWarning({
745
759
  provider: this.prefix,
746
760
  capability: "structured output",
747
761
  requested: `${outputSpec.name} (${this.prefix}/${modelId})`,
748
- reason: "the catalog reports structured_output: false for this model, so native response_format is unsupported.",
762
+ reason:
763
+ "the catalog reports structured_output: false for this model, so native response_format is unsupported.",
749
764
  fallback: "emulated JSON instruction + client validation (no response_format is sent)",
750
765
  });
751
766
  }
752
- const messages = await fullHistoryMessages(effectiveContext);
767
+ const messages = await fullHistoryMessages(effectiveContext, options?.prompts?.toolBridge);
753
768
  const body: ChatRequestBody = {
754
769
  model: modelId,
755
770
  messages,
@@ -758,10 +773,8 @@ export class OpenAICompatibleChatProvider implements Provider {
758
773
  const compatTools = toCompatTools(tools);
759
774
  if (compatTools) body.tools = compatTools;
760
775
  // Standard Chat Completions generation fields, only when the caller set them.
761
- Object.assign(body, mapGenerationParamsToChat(options, this.prefix, `${this.prefix}/${modelId}`));
762
- const toolChoice = mapToolChoiceToOpenAI(
763
- options?.toolChoice as "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
764
- );
776
+ Object.assign(body, mapGenerationParamsToChat(options));
777
+ const toolChoice = mapToolChoiceToOpenAI(options?.toolChoice, this.prefix);
765
778
  if (toolChoice !== undefined) {
766
779
  // Chat Completions nests pinned names under `function`; the canonical
767
780
  // mapper emits the flat Responses shape, so normalize here.
@@ -791,7 +804,7 @@ export class OpenAICompatibleChatProvider implements Provider {
791
804
  const noStreamOptions =
792
805
  (options as { streamOptions?: unknown } | undefined)?.streamOptions === false ||
793
806
  getEnv("COMPAT_NO_STREAM_OPTIONS") === "1" ||
794
- (options?.env?.["COMPAT_NO_STREAM_OPTIONS"] === "1");
807
+ options?.env?.["COMPAT_NO_STREAM_OPTIONS"] === "1";
795
808
  if (!noStreamOptions) {
796
809
  (body as unknown as Record<string, unknown>)["stream_options"] = { include_usage: true };
797
810
  }
@@ -807,11 +820,7 @@ export class OpenAICompatibleChatProvider implements Provider {
807
820
  sessionId: string | undefined,
808
821
  signal?: AbortSignal
809
822
  ): RequestInit {
810
- const headers: Record<string, string> = buildSessionHeaders(
811
- this.prefix,
812
- options?.headers,
813
- sessionId
814
- );
823
+ const headers: Record<string, string> = buildSessionHeaders(this.prefix, options?.headers, sessionId);
815
824
  // Strict-endpoint opt-out (pi sendSessionAffinityHeaders pattern):
816
825
  // `COMPAT_NO_SESSION_HEADERS=1` or per-call `sendSessionHeaders:false`
817
826
  // drops affinity headers that trigger CORS preflights / strict 400s.
@@ -844,7 +853,10 @@ export class OpenAICompatibleChatProvider implements Provider {
844
853
  sessionId: string | undefined,
845
854
  signal?: AbortSignal
846
855
  ): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
847
- const { signal: effective, cleanup } = combinedSignal(signal, timeoutFor(options as { timeoutMs?: number } | undefined));
856
+ const { signal: effective, cleanup } = combinedSignal(
857
+ signal,
858
+ timeoutFor(options as { timeoutMs?: number } | undefined)
859
+ );
848
860
  try {
849
861
  const res = await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, effective));
850
862
  const text = await res.text();
@@ -939,13 +951,10 @@ export class OpenAICompatibleChatProvider implements Provider {
939
951
  // Stateless turns never establish chains, but the session store still
940
952
  // records the turn so other providers' switch detection keeps working.
941
953
  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
- );
954
+ return parseResponse(response, this.prefix, clean, Date.now() - startTime, {
955
+ request: rawRequest,
956
+ response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response },
957
+ });
949
958
  };
950
959
 
951
960
  try {
@@ -1000,9 +1009,13 @@ export class OpenAICompatibleChatProvider implements Provider {
1000
1009
  // Transient failures (429/5xx/network) are retried here, before the
1001
1010
  // first byte; once the stream is live it is never retried.
1002
1011
  const res = await openStreamResponse({
1003
- fetchOnce: () => withAbortTimer(linked, streamTimeoutMs, () => (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))),
1012
+ fetchOnce: () =>
1013
+ withAbortTimer(linked, streamTimeoutMs, () =>
1014
+ (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))
1015
+ ),
1004
1016
  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),
1017
+ noBodyError: () =>
1018
+ toConciseProviderError(new Error("OpenAI-compatible streaming response had no body"), prefix, clean),
1006
1019
  retry: retryOptionsFrom(options),
1007
1020
  signal: linked.signal,
1008
1021
  label: { providerId: prefix, modelId: clean },
@@ -1011,7 +1024,11 @@ export class OpenAICompatibleChatProvider implements Provider {
1011
1024
  res.headers.forEach((v, k) => {
1012
1025
  responseHeaders[k] = v;
1013
1026
  });
1014
- const responseMeta: Record<string, unknown> & { status: number; statusText: string; headers: Record<string, string> } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
1027
+ const responseMeta: Record<string, unknown> & {
1028
+ status: number;
1029
+ statusText: string;
1030
+ headers: Record<string, string>;
1031
+ } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
1015
1032
  eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
1016
1033
 
1017
1034
  let text = "";
@@ -1059,7 +1076,8 @@ export class OpenAICompatibleChatProvider implements Provider {
1059
1076
  if (typeof msg["id"] === "string" && !responseId) {
1060
1077
  responseId = msg["id"] as string;
1061
1078
  }
1062
- const choice = (Array.isArray(msg["choices"]) ? (msg["choices"] as Record<string, unknown>[])[0] : undefined) ?? {};
1079
+ const choice =
1080
+ (Array.isArray(msg["choices"]) ? (msg["choices"] as Record<string, unknown>[])[0] : undefined) ?? {};
1063
1081
  const delta = (choice["delta"] as Record<string, unknown>) ?? {};
1064
1082
  // Text delta (content-free accounting frames carry "" — ignored).
1065
1083
  // 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,
@@ -59,7 +59,6 @@ import {
59
59
  newToolCallId,
60
60
  redactedHeaders,
61
61
  readErrorPayload,
62
- incompleteStreamError,
63
62
  hasUnfinishedToolCall,
64
63
  linkStreamAbort,
65
64
  withAbortTimer,
@@ -71,7 +70,6 @@ import {
71
70
  combinedSignal,
72
71
  timeoutFor,
73
72
  hasAuthHeader,
74
- clampToolCallId,
75
73
  openStreamResponse,
76
74
  retryOptionsFrom,
77
75
  headersToRecord,
@@ -93,6 +91,8 @@ import {
93
91
  augmentSystemPromptWithStructuredOutput,
94
92
  emitProviderWarning,
95
93
  warnIgnoredMediaResolution,
94
+ warnIgnoredVideoProcessing,
95
+ warnIgnoredToolMedia,
96
96
  } from "../providers.ts";
97
97
 
98
98
  // ---------------------------------------------------------------------------
@@ -124,7 +124,15 @@ interface ResponsesRequestBody {
124
124
  prompt_cache_key?: string;
125
125
  store?: boolean;
126
126
  stream?: boolean;
127
- text?: { format: { type: "json_schema"; name: string; schema: Record<string, unknown>; strict: boolean; description?: string } };
127
+ text?: {
128
+ format: {
129
+ type: "json_schema";
130
+ name: string;
131
+ schema: Record<string, unknown>;
132
+ strict: boolean;
133
+ description?: string;
134
+ };
135
+ };
128
136
  [k: string]: unknown;
129
137
  }
130
138
 
@@ -212,13 +220,9 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ResponseConte
212
220
  for (const part of parts) {
213
221
  if (part.type === "text" && part.text) {
214
222
  blocks.push({ type: "input_text", text: part.text });
215
- } else if (
216
- part.type === "image" ||
217
- part.type === "audio" ||
218
- part.type === "video" ||
219
- part.type === "file"
220
- ) {
221
- const raw = (part as { image?: unknown; audio?: unknown; video?: unknown; file?: unknown }).image ??
223
+ } else if (part.type === "image" || part.type === "audio" || part.type === "video" || part.type === "file") {
224
+ const raw =
225
+ (part as { image?: unknown; audio?: unknown; video?: unknown; file?: unknown }).image ??
222
226
  (part as { audio?: unknown }).audio ??
223
227
  (part as { video?: unknown }).video ??
224
228
  (part as { file?: unknown }).file;
@@ -319,7 +323,8 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
319
323
  if (m.role === "system") continue;
320
324
  if (m.role === "user") {
321
325
  if (typeof m.content === "string") {
322
- if (m.content) items.push({ type: "message", role: "user", content: [{ type: "input_text", text: m.content }] });
326
+ if (m.content)
327
+ items.push({ type: "message", role: "user", content: [{ type: "input_text", text: m.content }] });
323
328
  } else {
324
329
  const blocks = await contentPartsToBlocks(m.content);
325
330
  if (blocks.length > 0) items.push({ type: "message", role: "user", content: blocks });
@@ -363,7 +368,11 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
363
368
  // provider-minted ids that canonical history does not retain.
364
369
  }
365
370
  if (texts.length > 0) {
366
- items.push({ type: "message", role: "assistant", content: texts.map((t) => ({ type: "output_text", text: t })) });
371
+ items.push({
372
+ type: "message",
373
+ role: "assistant",
374
+ content: texts.map((t) => ({ type: "output_text", text: t })),
375
+ });
367
376
  }
368
377
  } else if (m.role === "tool") {
369
378
  if (typeof m.content === "string") {
@@ -544,6 +553,8 @@ export class OpenAIResponsesProvider implements Provider {
544
553
  // `12h` maps to `prompt_cache_retention: "24h"`; everything else stays on implicit prompt_cache_key affinity.
545
554
  const cache = resolveCache("openai", options?.cache, ["implicit", "12h"], `openai/${modelId}`);
546
555
  warnIgnoredMediaResolution("openai", context.messages, `openai/${modelId}`);
556
+ warnIgnoredVideoProcessing("openai", context.messages, `openai/${modelId}`);
557
+ warnIgnoredToolMedia("openai", context.messages, `openai/${modelId}`);
547
558
 
548
559
  // Structured outputs: native `text.format` when supported, else emulated
549
560
  // via instructions (catalog `structured_output: false` would 400).
@@ -557,7 +568,11 @@ export class OpenAIResponsesProvider implements Provider {
557
568
  }
558
569
  if (outputSpec && !modelSupportsStructuredOutput("openai", modelId)) {
559
570
  outputEmulated = true;
560
- effectiveSystemPrompt = augmentSystemPromptWithStructuredOutput(context.systemPrompt, outputSpec);
571
+ effectiveSystemPrompt = augmentSystemPromptWithStructuredOutput(
572
+ context.systemPrompt,
573
+ outputSpec,
574
+ options?.prompts?.structuredOutput
575
+ );
561
576
  emitProviderWarning({
562
577
  provider: "openai",
563
578
  capability: "structured output",
@@ -579,14 +594,12 @@ export class OpenAIResponsesProvider implements Provider {
579
594
  if (effectiveSystemPrompt) body.instructions = effectiveSystemPrompt;
580
595
  const aiTools = toOpenAITools(tools);
581
596
  if (aiTools) body.tools = aiTools;
582
- const toolChoice = mapToolChoiceToOpenAI(
583
- options?.toolChoice as "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
584
- );
597
+ const toolChoice = mapToolChoiceToOpenAI(options?.toolChoice);
585
598
  if (toolChoice !== undefined) body.tool_choice = toolChoice;
586
599
  if (effort) body.reasoning = { effort };
587
600
  if (serviceTier) body.service_tier = serviceTier;
588
- // max_output_tokens (stop + seed warn+drop: not on this wire).
589
- Object.assign(body, mapGenerationParamsToOpenAI(options, `openai/${modelId}`));
601
+ // max_output_tokens.
602
+ Object.assign(body, mapGenerationParamsToOpenAI(options));
590
603
  // Structured outputs: Responses `text.format{type:json_schema,name,schema,strict}`.
591
604
  // Skipped when emulated (instruction already in `instructions`).
592
605
  if (outputSpec && !outputEmulated) {
@@ -640,11 +653,7 @@ export class OpenAIResponsesProvider implements Provider {
640
653
  sessionId: string | undefined,
641
654
  signal?: AbortSignal
642
655
  ): RequestInit {
643
- const headers: Record<string, string> = buildSessionHeaders(
644
- "openai",
645
- options?.headers,
646
- sessionId
647
- );
656
+ const headers: Record<string, string> = buildSessionHeaders("openai", options?.headers, sessionId);
648
657
  // Case-insensitive merge so a lowercase `authorization` custom header
649
658
  // never coexists with the canonical bearer. Proxy flows
650
659
  // (`apiKey === "unused"`) preserve custom auth instead of overwriting.
@@ -662,7 +671,10 @@ export class OpenAIResponsesProvider implements Provider {
662
671
  sessionId: string | undefined,
663
672
  signal?: AbortSignal
664
673
  ): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
665
- const { signal: effective, cleanup } = combinedSignal(signal, timeoutFor(options as { timeoutMs?: number } | undefined));
674
+ const { signal: effective, cleanup } = combinedSignal(
675
+ signal,
676
+ timeoutFor(options as { timeoutMs?: number } | undefined)
677
+ );
666
678
  try {
667
679
  const res = await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, effective));
668
680
  const text = await res.text();
@@ -769,12 +781,10 @@ export class OpenAIResponsesProvider implements Provider {
769
781
  // Stateless turns never establish chains, but the session store still
770
782
  // records the turn so other providers' switch detection keeps working.
771
783
  noteProviderTurn(sessionId, "openai");
772
- return parseResponse(
773
- response,
774
- clean,
775
- Date.now() - startTime,
776
- { request: rawRequest, response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response } }
777
- );
784
+ return parseResponse(response, clean, Date.now() - startTime, {
785
+ request: rawRequest,
786
+ response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response },
787
+ });
778
788
  };
779
789
 
780
790
  try {
@@ -834,9 +844,13 @@ export class OpenAIResponsesProvider implements Provider {
834
844
  // Transient failures (429/5xx/network) are retried here, before the
835
845
  // first byte; once the stream is live it is never retried.
836
846
  const res = await openStreamResponse({
837
- fetchOnce: () => withAbortTimer(linked, streamTimeoutMs, () => (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))),
847
+ fetchOnce: () =>
848
+ withAbortTimer(linked, streamTimeoutMs, () =>
849
+ (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))
850
+ ),
838
851
  onHttpError: (r, text) => this.throwIfError(r.status, url, text, clean, headersToRecord(r.headers)),
839
- noBodyError: () => toConciseProviderError(new Error("OpenAI streaming response had no body"), "openai", clean),
852
+ noBodyError: () =>
853
+ toConciseProviderError(new Error("OpenAI streaming response had no body"), "openai", clean),
840
854
  retry: retryOptionsFrom(options),
841
855
  signal: linked.signal,
842
856
  label: { providerId: "openai", modelId: clean },
@@ -845,13 +859,20 @@ export class OpenAIResponsesProvider implements Provider {
845
859
  res.headers.forEach((v, k) => {
846
860
  responseHeaders[k] = v;
847
861
  });
848
- const responseMeta: Record<string, unknown> & { status: number; statusText: string; headers: Record<string, string> } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
862
+ const responseMeta: Record<string, unknown> & {
863
+ status: number;
864
+ statusText: string;
865
+ headers: Record<string, string>;
866
+ } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
849
867
  eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
850
868
 
851
869
  let text = "";
852
870
  let thinking = "";
853
871
  // Tool calls keyed by output_index: { itemId, callId, name, startArgs, deltaArgs }.
854
- const calls = new Map<number, { itemId: string; callId: string; name: string; startArgs: string; deltaArgs: string }>();
872
+ const calls = new Map<
873
+ number,
874
+ { itemId: string; callId: string; name: string; startArgs: string; deltaArgs: string }
875
+ >();
855
876
  let usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
856
877
  let finishReason = "stop";
857
878
  let sawFinishReason = false;
@@ -892,10 +913,7 @@ export class OpenAIResponsesProvider implements Provider {
892
913
  } else if (type === "response.output_text.delta" && typeof msg["delta"] === "string") {
893
914
  text += msg["delta"] as string;
894
915
  eventStream.push({ type: "text_delta", delta: msg["delta"] as string, partialText: text });
895
- } else if (
896
- type === "response.refusal.delta" ||
897
- type === "response.output_refusal.delta"
898
- ) {
916
+ } else if (type === "response.refusal.delta" || type === "response.output_refusal.delta") {
899
917
  // Refusal deltas carry the filter text; surface as text and
900
918
  // remember for a `refusal` finish reason.
901
919
  if (typeof msg["delta"] === "string" && msg["delta"]) {
@@ -908,7 +926,11 @@ export class OpenAIResponsesProvider implements Provider {
908
926
  typeof msg["delta"] === "string"
909
927
  ) {
910
928
  thinking += msg["delta"] as string;
911
- eventStream.push({ type: "thinking_delta", thinkingDelta: msg["delta"] as string, partialThinking: thinking });
929
+ eventStream.push({
930
+ type: "thinking_delta",
931
+ thinkingDelta: msg["delta"] as string,
932
+ partialThinking: thinking,
933
+ });
912
934
  } else if (type === "response.output_item.added") {
913
935
  const index = (msg["output_index"] as number) ?? 0;
914
936
  const item = (msg["item"] as Record<string, unknown>) ?? {};
@@ -957,7 +979,8 @@ export class OpenAIResponsesProvider implements Provider {
957
979
  // Non-streaming maps `incomplete` → `length` (max tokens). Streaming
958
980
  // must do the same — otherwise truncated turns misreport `stop`.
959
981
  // Content-filter incompletes surface as `refusal`.
960
- const incompleteReason = (response as { incomplete_details?: { reason?: string } }).incomplete_details?.reason;
982
+ const incompleteReason = (response as { incomplete_details?: { reason?: string } }).incomplete_details
983
+ ?.reason;
961
984
  if (response.status === "incomplete") {
962
985
  finishReason = incompleteReason === "content_filter" ? "refusal" : "length";
963
986
  sawFinishReason = true;