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
@@ -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,
@@ -124,7 +122,15 @@ interface ResponsesRequestBody {
124
122
  prompt_cache_key?: string;
125
123
  store?: boolean;
126
124
  stream?: boolean;
127
- 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
+ };
128
134
  [k: string]: unknown;
129
135
  }
130
136
 
@@ -212,13 +218,9 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ResponseConte
212
218
  for (const part of parts) {
213
219
  if (part.type === "text" && part.text) {
214
220
  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 ??
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 ??
222
224
  (part as { audio?: unknown }).audio ??
223
225
  (part as { video?: unknown }).video ??
224
226
  (part as { file?: unknown }).file;
@@ -319,7 +321,8 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
319
321
  if (m.role === "system") continue;
320
322
  if (m.role === "user") {
321
323
  if (typeof m.content === "string") {
322
- 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 }] });
323
326
  } else {
324
327
  const blocks = await contentPartsToBlocks(m.content);
325
328
  if (blocks.length > 0) items.push({ type: "message", role: "user", content: blocks });
@@ -363,7 +366,11 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
363
366
  // provider-minted ids that canonical history does not retain.
364
367
  }
365
368
  if (texts.length > 0) {
366
- 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
+ });
367
374
  }
368
375
  } else if (m.role === "tool") {
369
376
  if (typeof m.content === "string") {
@@ -585,8 +592,8 @@ export class OpenAIResponsesProvider implements Provider {
585
592
  if (toolChoice !== undefined) body.tool_choice = toolChoice;
586
593
  if (effort) body.reasoning = { effort };
587
594
  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}`));
595
+ // max_output_tokens.
596
+ Object.assign(body, mapGenerationParamsToOpenAI(options));
590
597
  // Structured outputs: Responses `text.format{type:json_schema,name,schema,strict}`.
591
598
  // Skipped when emulated (instruction already in `instructions`).
592
599
  if (outputSpec && !outputEmulated) {
@@ -640,11 +647,7 @@ export class OpenAIResponsesProvider implements Provider {
640
647
  sessionId: string | undefined,
641
648
  signal?: AbortSignal
642
649
  ): RequestInit {
643
- const headers: Record<string, string> = buildSessionHeaders(
644
- "openai",
645
- options?.headers,
646
- sessionId
647
- );
650
+ const headers: Record<string, string> = buildSessionHeaders("openai", options?.headers, sessionId);
648
651
  // Case-insensitive merge so a lowercase `authorization` custom header
649
652
  // never coexists with the canonical bearer. Proxy flows
650
653
  // (`apiKey === "unused"`) preserve custom auth instead of overwriting.
@@ -662,7 +665,10 @@ export class OpenAIResponsesProvider implements Provider {
662
665
  sessionId: string | undefined,
663
666
  signal?: AbortSignal
664
667
  ): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
665
- 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
+ );
666
672
  try {
667
673
  const res = await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, effective));
668
674
  const text = await res.text();
@@ -769,12 +775,10 @@ export class OpenAIResponsesProvider implements Provider {
769
775
  // Stateless turns never establish chains, but the session store still
770
776
  // records the turn so other providers' switch detection keeps working.
771
777
  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
- );
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
+ });
778
782
  };
779
783
 
780
784
  try {
@@ -834,9 +838,13 @@ export class OpenAIResponsesProvider implements Provider {
834
838
  // Transient failures (429/5xx/network) are retried here, before the
835
839
  // first byte; once the stream is live it is never retried.
836
840
  const res = await openStreamResponse({
837
- 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
+ ),
838
845
  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),
846
+ noBodyError: () =>
847
+ toConciseProviderError(new Error("OpenAI streaming response had no body"), "openai", clean),
840
848
  retry: retryOptionsFrom(options),
841
849
  signal: linked.signal,
842
850
  label: { providerId: "openai", modelId: clean },
@@ -845,13 +853,20 @@ export class OpenAIResponsesProvider implements Provider {
845
853
  res.headers.forEach((v, k) => {
846
854
  responseHeaders[k] = v;
847
855
  });
848
- 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 };
849
861
  eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
850
862
 
851
863
  let text = "";
852
864
  let thinking = "";
853
865
  // 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 }>();
866
+ const calls = new Map<
867
+ number,
868
+ { itemId: string; callId: string; name: string; startArgs: string; deltaArgs: string }
869
+ >();
855
870
  let usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
856
871
  let finishReason = "stop";
857
872
  let sawFinishReason = false;
@@ -892,10 +907,7 @@ export class OpenAIResponsesProvider implements Provider {
892
907
  } else if (type === "response.output_text.delta" && typeof msg["delta"] === "string") {
893
908
  text += msg["delta"] as string;
894
909
  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
- ) {
910
+ } else if (type === "response.refusal.delta" || type === "response.output_refusal.delta") {
899
911
  // Refusal deltas carry the filter text; surface as text and
900
912
  // remember for a `refusal` finish reason.
901
913
  if (typeof msg["delta"] === "string" && msg["delta"]) {
@@ -908,7 +920,11 @@ export class OpenAIResponsesProvider implements Provider {
908
920
  typeof msg["delta"] === "string"
909
921
  ) {
910
922
  thinking += msg["delta"] as string;
911
- 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
+ });
912
928
  } else if (type === "response.output_item.added") {
913
929
  const index = (msg["output_index"] as number) ?? 0;
914
930
  const item = (msg["item"] as Record<string, unknown>) ?? {};
@@ -957,7 +973,8 @@ export class OpenAIResponsesProvider implements Provider {
957
973
  // Non-streaming maps `incomplete` → `length` (max tokens). Streaming
958
974
  // must do the same — otherwise truncated turns misreport `stop`.
959
975
  // Content-filter incompletes surface as `refusal`.
960
- const incompleteReason = (response as { incomplete_details?: { reason?: string } }).incomplete_details?.reason;
976
+ const incompleteReason = (response as { incomplete_details?: { reason?: string } }).incomplete_details
977
+ ?.reason;
961
978
  if (response.status === "incomplete") {
962
979
  finishReason = incompleteReason === "content_filter" ? "refusal" : "length";
963
980
  sawFinishReason = true;
@@ -51,7 +51,6 @@ import {
51
51
  newToolCallId,
52
52
  redactedHeaders,
53
53
  readErrorPayload,
54
- incompleteStreamError,
55
54
  hasUnfinishedToolCall,
56
55
  linkStreamAbort,
57
56
  withAbortTimer,
@@ -123,7 +122,10 @@ interface ChatRequestBody {
123
122
  service_tier?: string;
124
123
  session_id?: string;
125
124
  cache_control?: { type: "ephemeral"; ttl: "5m" | "1h" };
126
- response_format?: { type: "json_schema"; json_schema: { name: string; schema: Record<string, unknown>; strict: boolean; description?: string } };
125
+ response_format?: {
126
+ type: "json_schema";
127
+ json_schema: { name: string; schema: Record<string, unknown>; strict: boolean; description?: string };
128
+ };
127
129
  stream?: boolean;
128
130
  [k: string]: unknown;
129
131
  }
@@ -173,7 +175,13 @@ function cleanModelId(model: string | ModelSpec): string {
173
175
  }
174
176
 
175
177
  // The HTTP status of an error chunk inside a stream (the HTTP status itself is 200): its numeric `code`, else the status of its retryable `error_type` (errors-and-debugging.md, "Typed Error Codes"); in: the chunk's error, out: streamInto, where it makes the error retryable.
176
- const RETRYABLE_ERROR_TYPE_STATUS: Record<string, number> = { rate_limit_exceeded: 429, provider_overloaded: 503, provider_unavailable: 502, timeout: 504, server: 500 };
178
+ const RETRYABLE_ERROR_TYPE_STATUS: Record<string, number> = {
179
+ rate_limit_exceeded: 429,
180
+ provider_overloaded: 503,
181
+ provider_unavailable: 502,
182
+ timeout: 504,
183
+ server: 500,
184
+ };
177
185
  // HTTP-like status of an error chunk inside a stream, or undefined when unknown; in: streamInto's error branch, out: the status that makes the error retryable.
178
186
  function midStreamStatus(code: unknown, errorType: unknown): number | undefined {
179
187
  if (typeof code === "number" && Number.isInteger(code) && code >= 100 && code <= 599) return code;
@@ -195,7 +203,18 @@ function toOpenRouterTools(tools?: StandardToolDeclaration[]): Array<Record<stri
195
203
  }
196
204
 
197
205
  // The `input_audio.format` for a MIME type: Chat Completions takes any format string the provider supports (audio.md: wav, mp3, aiff, aac, ogg, flac, m4a, pcm16 ...), so the real format is sent, never a guess; in: an audio part's MIME type, out: contentPartsToBlocks.
198
- const AUDIO_FORMAT: Record<string, string> = { wav: "wav", wave: "wav", mpeg: "mp3", mp3: "mp3", mp4: "m4a", m4a: "m4a", aac: "aac", ogg: "ogg", flac: "flac", aiff: "aiff" };
206
+ const AUDIO_FORMAT: Record<string, string> = {
207
+ wav: "wav",
208
+ wave: "wav",
209
+ mpeg: "mp3",
210
+ mp3: "mp3",
211
+ mp4: "m4a",
212
+ m4a: "m4a",
213
+ aac: "aac",
214
+ ogg: "ogg",
215
+ flac: "flac",
216
+ aiff: "aiff",
217
+ };
199
218
  // The `input_audio.format` for an audio MIME type (see AUDIO_FORMAT above); in: contentPartsToBlocks, out: the format string.
200
219
  function audioFormatFor(mimeType?: string): string {
201
220
  const subtype = (mimeType || "").toLowerCase().split(";")[0]!.split("/")[1]?.replace(/^x-/, "") ?? "";
@@ -203,7 +222,12 @@ function audioFormatFor(mimeType?: string): string {
203
222
  }
204
223
 
205
224
  // `ImagePart.resolution` as OpenRouter's `image_url.detail` (api-ref ChatContentImage: auto, low, high, original; `original` is downgraded to `high` where unsupported); videos have no such field; in: an image part, out: contentPartsToBlocks.
206
- const IMAGE_DETAIL: Record<string, "low" | "auto" | "high" | "original"> = { low: "low", medium: "auto", high: "high", ultra_high: "original" };
225
+ const IMAGE_DETAIL: Record<string, "low" | "auto" | "high" | "original"> = {
226
+ low: "low",
227
+ medium: "auto",
228
+ high: "high",
229
+ ultra_high: "original",
230
+ };
207
231
 
208
232
  // True when a request's messages carry an image or video by web link (the router fetches those itself); in: the built messages, out: throwIfError's hint.
209
233
  function hasRemoteMedia(messages: ChatMessage[]): boolean {
@@ -211,7 +235,9 @@ function hasRemoteMedia(messages: ChatMessage[]): boolean {
211
235
  (m) =>
212
236
  Array.isArray(m.content) &&
213
237
  (m.content as ChatContentPart[]).some((p) => {
214
- const url = (p as { image_url?: { url?: string }; video_url?: { url?: string } }).image_url?.url ?? (p as { video_url?: { url?: string } }).video_url?.url;
238
+ const url =
239
+ (p as { image_url?: { url?: string }; video_url?: { url?: string } }).image_url?.url ??
240
+ (p as { video_url?: { url?: string } }).video_url?.url;
215
241
  return typeof url === "string" && /^https?:\/\//.test(url);
216
242
  })
217
243
  );
@@ -258,10 +284,7 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ChatContentPa
258
284
  const mimeType = (part as { mimeType?: string }).mimeType;
259
285
  // OpenAI chat audio shape (router parsed it live; capability routing
260
286
  // decides per model). Always inline — `input_audio` carries data only.
261
- const norm = await normalizeMediaInput(
262
- raw as string | Uint8Array | ArrayBuffer,
263
- mimeType
264
- );
287
+ const norm = await normalizeMediaInput(raw as string | Uint8Array | ArrayBuffer, mimeType);
265
288
  blocks.push({
266
289
  type: "input_audio",
267
290
  input_audio: { data: norm.base64Data, format: audioFormatFor(norm.mimeType) },
@@ -273,7 +296,12 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ChatContentPa
273
296
  const fileData =
274
297
  typeof raw === "string" && (raw.startsWith("http://") || raw.startsWith("https://"))
275
298
  ? raw
276
- : (await normalizeMediaInput(raw as string | Uint8Array | ArrayBuffer, (part as { mimeType?: string }).mimeType)).dataUrl;
299
+ : (
300
+ await normalizeMediaInput(
301
+ raw as string | Uint8Array | ArrayBuffer,
302
+ (part as { mimeType?: string }).mimeType
303
+ )
304
+ ).dataUrl;
277
305
  blocks.push({
278
306
  type: "file",
279
307
  file: { ...(typeof filename === "string" && filename ? { filename } : {}), file_data: fileData },
@@ -329,10 +357,16 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
329
357
  messages.push({ role: "assistant", content: "I have processed the tool results." });
330
358
  }
331
359
  if (typeof m.content === "string") {
332
- if (m.content) { messages.push({ role: "user", content: m.content }); lastRole = "user"; }
360
+ if (m.content) {
361
+ messages.push({ role: "user", content: m.content });
362
+ lastRole = "user";
363
+ }
333
364
  } else {
334
365
  const blocks = await contentPartsToBlocks(m.content);
335
- if (blocks.length > 0) { messages.push({ role: "user", content: blocks }); lastRole = "user"; }
366
+ if (blocks.length > 0) {
367
+ messages.push({ role: "user", content: blocks });
368
+ lastRole = "user";
369
+ }
336
370
  }
337
371
  } else if (m.role === "assistant") {
338
372
  if (typeof m.content === "string") {
@@ -414,7 +448,8 @@ function mapUsage(raw?: ChatUsage): TokenUsage {
414
448
  cacheWriteTokens: raw?.prompt_tokens_details?.cache_write_tokens ?? 0,
415
449
  thinkingTokens: raw?.completion_tokens_details?.reasoning_tokens ?? 0,
416
450
  };
417
- if (typeof raw?.cost === "number" && raw.cost >= 0) { // an explicit 0 (a free model, a response-cache hit) is a known cost, not a missing one
451
+ if (typeof raw?.cost === "number" && raw.cost >= 0) {
452
+ // an explicit 0 (a free model, a response-cache hit) is a known cost, not a missing one
418
453
  usage.cost = { totalCost: raw.cost };
419
454
  }
420
455
  return usage;
@@ -460,7 +495,12 @@ export class OpenRouterChatCompletionsProvider implements Provider {
460
495
  const { effort } = mapThinkingLevelToOpenRouterChat(options?.thinking?.level);
461
496
  const serviceTier = mapServiceTierToOpenRouter(options?.serviceTier);
462
497
  // Anthropic models take a top-level `cache_control` TTL; every other route caches implicitly (docs: prompt-caching.md).
463
- const cache = resolveCache("openrouter", options?.cache, /^~?anthropic\//.test(modelId) ? ["implicit", "5m", "1h"] : ["implicit"], `openrouter/${modelId}`);
498
+ const cache = resolveCache(
499
+ "openrouter",
500
+ options?.cache,
501
+ /^~?anthropic\//.test(modelId) ? ["implicit", "5m", "1h"] : ["implicit"],
502
+ `openrouter/${modelId}`
503
+ );
464
504
  warnIgnoredMediaResolution("openrouter", context.messages, `openrouter/${modelId}`, ["video"]);
465
505
 
466
506
  // Structured outputs: native `response_format` when supported, else
@@ -485,7 +525,8 @@ export class OpenRouterChatCompletionsProvider implements Provider {
485
525
  provider: "openrouter",
486
526
  capability: "structured output",
487
527
  requested: `${outputSpec.name} (openrouter/${modelId})`,
488
- reason: "the catalog reports structured_output: false for this model, so native response_format is unsupported.",
528
+ reason:
529
+ "the catalog reports structured_output: false for this model, so native response_format is unsupported.",
489
530
  fallback: "emulated JSON instruction + client validation (no response_format is sent)",
490
531
  });
491
532
  }
@@ -505,7 +546,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
505
546
  if (serviceTier) body.service_tier = serviceTier;
506
547
  if (cache === "5m" || cache === "1h") body.cache_control = { type: "ephemeral", ttl: cache };
507
548
  // Documented Chat Completions generation controls (only those the caller set).
508
- Object.assign(body, mapGenerationParamsToChat(options, "openrouter", `openrouter/${modelId}`));
549
+ Object.assign(body, mapGenerationParamsToChat(options));
509
550
  // Structured outputs: Chat Completions `response_format{type:json_schema,json_schema}`.
510
551
  // Skipped when emulated (unsupported model): instruction already in messages.
511
552
  if (outputSpec && !outputEmulated) {
@@ -537,11 +578,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
537
578
  ): RequestInit {
538
579
  // Attribution + session headers mirror the previous transport exactly
539
580
  // (HTTP-Referer / X-Title / x-session-id / x-client-request-id).
540
- const headers: Record<string, string> = buildSessionHeaders(
541
- "openrouter",
542
- options?.headers,
543
- sessionId
544
- );
581
+ const headers: Record<string, string> = buildSessionHeaders("openrouter", options?.headers, sessionId);
545
582
  setHeaderCaseInsensitive(headers, "Content-Type", "application/json");
546
583
  if (apiKey !== "unused") setHeaderCaseInsensitive(headers, "Authorization", `Bearer ${apiKey}`);
547
584
  return { method: "POST", headers, body: JSON.stringify(body), signal };
@@ -593,10 +630,16 @@ export class OpenRouterChatCompletionsProvider implements Provider {
593
630
  options?: ProviderRequestOptions
594
631
  ): Promise<ProviderGenerateResult> {
595
632
  // The whole call stays bounded by `timeoutMs`; the stream's own timer only covers the wait for its first byte.
596
- const { signal, cleanup } = combinedSignal(options?.signal, timeoutFor(options as { timeoutMs?: number } | undefined));
633
+ const { signal, cleanup } = combinedSignal(
634
+ options?.signal,
635
+ timeoutFor(options as { timeoutMs?: number } | undefined)
636
+ );
597
637
  try {
598
638
  // Nobody reads the deltas here, so the stream keeps none (a burst of frames would overflow its buffer for nothing).
599
- const events = this.streamInto(new AssistantMessageEventStream({ buffer: false }), model, context, { ...options, signal });
639
+ const events = this.streamInto(new AssistantMessageEventStream({ buffer: false }), model, context, {
640
+ ...options,
641
+ signal,
642
+ });
600
643
  const res = await events.result();
601
644
  return {
602
645
  text: res.text,
@@ -675,9 +718,14 @@ export class OpenRouterChatCompletionsProvider implements Provider {
675
718
  // Transient failures (429/5xx/network) are retried here, before the
676
719
  // first byte; once the stream is live it is never retried.
677
720
  const res = await openStreamResponse({
678
- fetchOnce: () => withAbortTimer(linked, streamTimeoutMs, () => (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))),
679
- onHttpError: (r, text) => this.throwIfError(r.status, url, text, clean, headersToRecord(r.headers), hasRemoteMedia(body.messages)),
680
- noBodyError: () => toConciseProviderError(new Error("OpenRouter streaming response had no body"), "openrouter", clean),
721
+ fetchOnce: () =>
722
+ withAbortTimer(linked, streamTimeoutMs, () =>
723
+ (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))
724
+ ),
725
+ onHttpError: (r, text) =>
726
+ this.throwIfError(r.status, url, text, clean, headersToRecord(r.headers), hasRemoteMedia(body.messages)),
727
+ noBodyError: () =>
728
+ toConciseProviderError(new Error("OpenRouter streaming response had no body"), "openrouter", clean),
681
729
  retry: retryOptionsFrom(options),
682
730
  signal: linked.signal,
683
731
  label: { providerId: "openrouter", modelId: clean },
@@ -686,7 +734,11 @@ export class OpenRouterChatCompletionsProvider implements Provider {
686
734
  res.headers.forEach((v, k) => {
687
735
  responseHeaders[k] = v;
688
736
  });
689
- const responseMeta: Record<string, unknown> & { status: number; statusText: string; headers: Record<string, string> } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
737
+ const responseMeta: Record<string, unknown> & {
738
+ status: number;
739
+ statusText: string;
740
+ headers: Record<string, string>;
741
+ } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
690
742
  eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
691
743
 
692
744
  let text = "";
@@ -721,7 +773,9 @@ export class OpenRouterChatCompletionsProvider implements Provider {
721
773
  }
722
774
  // Mid-stream provider error: top-level `error`, HTTP stays 200.
723
775
  // Must surface — resolving empty success hides it.
724
- const topError = msg["error"] as { message?: string; code?: string | number; metadata?: { error_type?: string } } | undefined;
776
+ const topError = msg["error"] as
777
+ | { message?: string; code?: string | number; metadata?: { error_type?: string } }
778
+ | undefined;
725
779
  if (topError && typeof topError === "object") {
726
780
  const message =
727
781
  typeof topError.message === "string" && topError.message
@@ -738,7 +792,8 @@ export class OpenRouterChatCompletionsProvider implements Provider {
738
792
  if (typeof msg["id"] === "string" && !responseId) {
739
793
  responseId = msg["id"] as string;
740
794
  }
741
- const choice = (Array.isArray(msg["choices"]) ? (msg["choices"] as Record<string, unknown>[])[0] : undefined) ?? {};
795
+ const choice =
796
+ (Array.isArray(msg["choices"]) ? (msg["choices"] as Record<string, unknown>[])[0] : undefined) ?? {};
742
797
  const delta = (choice["delta"] as Record<string, unknown>) ?? {};
743
798
  // Text delta (content-free accounting frames carry "" — ignored).
744
799
  // Refusal deltas surface as text with a `refusal` finish.
@@ -31,12 +31,7 @@ providerRegistry.set("openai", openaiProvider);
31
31
  // MODEL="cerebras/gpt-oss-120b" + CEREBRAS_API_KEY + CEREBRAS_BASE_URL
32
32
  // MODEL="ollama/qwen2.5-coder" + OLLAMA_BASE_URL (key optional for local)
33
33
  // ---------------------------------------------------------------------------
34
- const FIRST_CLASS = new Set([
35
- "google",
36
- "gemini",
37
- "openrouter",
38
- "openai",
39
- ]);
34
+ const FIRST_CLASS = new Set(["google", "gemini", "openrouter", "openai"]);
40
35
 
41
36
  // Removed providers: fail loudly instead of misrouting. OpenCode free models
42
37
  // are harness-only since 2026, so the opencode/opencode-go prefixes no longer
@@ -168,9 +163,7 @@ export function resolveModel(model: string | ModelSpec): ResolvedModel {
168
163
  // Try catalog first for any provider/model
169
164
  const tryCatalogLookup = (prov: string, mid: string): ModelSpec | undefined => {
170
165
  return (
171
- getModelFromCatalog(prov, mid) ||
172
- getModelFromCatalog(prov.toLowerCase(), mid) ||
173
- getModelFromCatalog(mid, mid)
166
+ getModelFromCatalog(prov, mid) || getModelFromCatalog(prov.toLowerCase(), mid) || getModelFromCatalog(mid, mid)
174
167
  );
175
168
  };
176
169
 
@@ -194,11 +187,7 @@ export function resolveModel(model: string | ModelSpec): ResolvedModel {
194
187
  }
195
188
 
196
189
  // 2. Unified layer: any non-first-class prefix routes to auto-created custom provider
197
- const isExplicitOther = [
198
- "google",
199
- "gemini",
200
- "openrouter",
201
- ].includes(providerPrefix);
190
+ const isExplicitOther = ["google", "gemini", "openrouter"].includes(providerPrefix);
202
191
 
203
192
  if (!isExplicitOther && providerPrefix !== "openai") {
204
193
  const envName = providerPrefix.toUpperCase().replace(/[^A-Z0-9]/g, "_");
@@ -210,8 +199,7 @@ export function resolveModel(model: string | ModelSpec): ResolvedModel {
210
199
  getEnv(`${envName}_BASE_API_KEY`)
211
200
  );
212
201
 
213
- const looksLikeOpenRouterScope =
214
- parts.length === 2 && modelStr.includes(":") && !hasDedicatedEnv;
202
+ const looksLikeOpenRouterScope = parts.length === 2 && modelStr.includes(":") && !hasDedicatedEnv;
215
203
 
216
204
  if (!looksLikeOpenRouterScope) {
217
205
  const custom = ensureCustomProvider(providerPrefix);
@@ -251,9 +239,7 @@ export function resolveModel(model: string | ModelSpec): ResolvedModel {
251
239
  }
252
240
 
253
241
  // Check catalog for this exact provider/model
254
- const catalogSpec =
255
- tryCatalogLookup(providerPrefix, remainingModel) ||
256
- tryCatalogLookup(providerPrefix, modelStr);
242
+ const catalogSpec = tryCatalogLookup(providerPrefix, remainingModel) || tryCatalogLookup(providerPrefix, modelStr);
257
243
  if (catalogSpec) {
258
244
  const internalProvider = getProvider(catalogSpec.provider);
259
245
  return { provider: internalProvider, modelId: catalogSpec.id, modelSpec: catalogSpec };
@@ -277,8 +263,7 @@ export function resolveModel(model: string | ModelSpec): ResolvedModel {
277
263
 
278
264
  // Fallback custom provider
279
265
  const fallbackCustom = ensureCustomProvider(providerPrefix);
280
- const fallbackSpec =
281
- fallbackCustom.getModel(remainingModel) || fallbackCustom.getModel(modelStr);
266
+ const fallbackSpec = fallbackCustom.getModel(remainingModel) || fallbackCustom.getModel(modelStr);
282
267
  return {
283
268
  provider: fallbackCustom,
284
269
  modelId: remainingModel,