agent-accelerator 0.5.6 → 0.5.8

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 (57) hide show
  1. package/README.md +10 -17
  2. package/package.json +1 -1
  3. package/src/agent/agent.ts +92 -57
  4. package/src/agent/architecture.md +25 -0
  5. package/src/agent/context.ts +7 -3
  6. package/src/agent/delegation.ts +30 -96
  7. package/src/agent/loop.ts +54 -56
  8. package/src/agent/subagent.ts +4 -4
  9. package/src/agent/worker.ts +1 -3
  10. package/src/architecture.md +28 -0
  11. package/src/index.ts +13 -80
  12. package/src/models/architecture.md +14 -0
  13. package/src/models/catalog-cache.ts +10 -7
  14. package/src/models/catalog.ts +20 -13
  15. package/src/providers/architecture.md +24 -0
  16. package/src/providers/google.ts +24 -4
  17. package/src/providers/openai-compat.ts +29 -7
  18. package/src/providers/openai.ts +22 -6
  19. package/src/providers/openrouter.ts +14 -2
  20. package/src/providers/registry.ts +1 -0
  21. package/src/providers/shared.ts +4 -4
  22. package/src/providers.ts +9 -53
  23. package/src/scheduler/architecture.md +22 -0
  24. package/src/scheduler/scheduler.ts +131 -56
  25. package/src/scheduler/store.ts +274 -54
  26. package/src/scheduler/time.ts +2 -0
  27. package/src/session/architecture.md +19 -0
  28. package/src/session/store.ts +37 -22
  29. package/src/session/usage.ts +1 -0
  30. package/src/streaming/architecture.md +13 -0
  31. package/src/streaming/event-stream.ts +16 -11
  32. package/src/telemetry-file.ts +74 -24
  33. package/src/telemetry.ts +3 -2
  34. package/src/tools/architecture.md +19 -0
  35. package/src/tools/executor.ts +14 -1
  36. package/src/tools/schema.ts +1 -0
  37. package/src/tools/subagent-result.ts +1 -0
  38. package/src/tools/tool.ts +8 -8
  39. package/src/types/agent.ts +22 -20
  40. package/src/types/architecture.md +20 -0
  41. package/src/types/response.ts +8 -8
  42. package/src/types/scheduler.ts +2 -2
  43. package/src/types/tool.ts +5 -5
  44. package/src/update-models.ts +2 -2
  45. package/src/utils/architecture.md +25 -0
  46. package/src/utils/base64.ts +2 -0
  47. package/src/utils/cache.ts +6 -4
  48. package/src/utils/durable.ts +78 -3
  49. package/src/utils/env.ts +1 -1
  50. package/src/utils/errors.ts +4 -0
  51. package/src/utils/headers.ts +7 -4
  52. package/src/utils/home.ts +1 -1
  53. package/src/utils/media.ts +1 -0
  54. package/src/utils/raw-capture.ts +3 -0
  55. package/src/utils/retry.ts +2 -1
  56. package/src/utils/safe-io.ts +4 -0
  57. package/src/utils/session.ts +10 -166
@@ -192,10 +192,12 @@ interface ChatResponse {
192
192
  // Request building (canonical -> Chat Completions)
193
193
  // ---------------------------------------------------------------------------
194
194
 
195
+ // Env-variable prefix of a custom provider (`my-llm` becomes `MY_LLM`); in: key and URL lookups, out: the prefix.
195
196
  function envPrefixOf(prefix: string): string {
196
197
  return prefix.toUpperCase().replace(/[^A-Z0-9]/g, "_");
197
198
  }
198
199
 
200
+ // Endpoint from the option, the provider's configuration or {PREFIX}_BASE_URL; throws ConfigError when none (never another vendor's URL); in: each request, out: the base URL.
199
201
  function resolveBaseUrl(
200
202
  prefix: string,
201
203
  configuredBaseUrl: string | undefined,
@@ -221,6 +223,7 @@ function resolveBaseUrl(
221
223
  return resolved.replace(/\/+$/, "");
222
224
  }
223
225
 
226
+ // Key from the option, the provider's configuration or {PREFIX}_API_KEY, or `unused` when custom headers carry the auth; in: each request, out: the key or undefined.
224
227
  function resolveApiKey(
225
228
  prefix: string,
226
229
  configuredApiKey: string | undefined,
@@ -282,6 +285,7 @@ function cleanModelId(prefix: string, model: string | ModelSpec): string {
282
285
  return rawId.replace(new RegExp(`^${escaped}/`, "i"), "");
283
286
  }
284
287
 
288
+ // Canonical tool declarations as Chat Completions function tools; in: options.tools, out: request `tools`.
285
289
  function toCompatTools(tools?: StandardToolDeclaration[]): Array<Record<string, unknown>> | undefined {
286
290
  if (!tools || tools.length === 0) return undefined;
287
291
  return tools.map((t) => ({
@@ -309,6 +313,7 @@ function warnThinkingDropped(prefix: string, thinking: ThinkingConfig | undefine
309
313
  });
310
314
  }
311
315
 
316
+ // `wav` or `mp3`, the only formats Chat audio input takes here; in: contentPartsToBlocks, out: input_audio.format.
312
317
  function audioFormatFor(mimeType?: string): string {
313
318
  const mime = (mimeType || "").toLowerCase();
314
319
  if (mime.includes("wav")) return "wav";
@@ -354,6 +359,7 @@ function assertNoVideoOrFileParts(
354
359
  throw err;
355
360
  }
356
361
 
362
+ // Text, image (link or data URL) and audio parts as Chat content blocks; in: a user message's parts, out: fullHistoryMessages.
357
363
  async function contentPartsToBlocks(parts: ContentPart[]): Promise<ChatContentPart[]> {
358
364
  const blocks: ChatContentPart[] = [];
359
365
  for (const part of parts) {
@@ -388,6 +394,7 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ChatContentPa
388
394
  return blocks;
389
395
  }
390
396
 
397
+ // A tool result as the text of a tool message; in: fullHistoryMessages, out: a string.
391
398
  function resultToOutput(result: unknown): string {
392
399
  if (typeof result === "string") return result;
393
400
  return safeStringify(result);
@@ -399,7 +406,7 @@ function resultToOutput(result: unknown): string {
399
406
  * resent — except echoed Gemini thought signatures, which strict
400
407
  * OpenAI-compatible endpoints require on tool calls they generated.
401
408
  *
402
- * Q-18: signatures are namespaced — only echo when the stored provider is
409
+ * Signatures are namespaced — only echo when the stored provider is
403
410
  * `google` (or untagged legacy). Signatures minted by other providers never
404
411
  * leak onto this transport.
405
412
  */
@@ -461,7 +468,7 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
461
468
  // Echo provider-issued thought signatures verbatim so endpoints
462
469
  // that minted them (Gemini behind a compat proxy) keep working.
463
470
  // Only present when a previous turn captured one — other endpoints
464
- // never see this field. Q-18: echo only google/untagged signatures.
471
+ // never see this field. Echo only google/untagged signatures.
465
472
  const storedProvider = (part as { thoughtSignatureProvider?: unknown }).thoughtSignatureProvider;
466
473
  if (part.thoughtSignature && shouldEchoSignature(storedProvider, "google")) {
467
474
  call.extra_content = { google: { thought_signature: part.thoughtSignature } };
@@ -502,6 +509,7 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
502
509
  // Response mapping (Chat Completions -> canonical)
503
510
  // ---------------------------------------------------------------------------
504
511
 
512
+ // Chat usage as TokenUsage, clamping cache reads to input; in: a reply or the last stream chunk, out: TokenUsage.
505
513
  function mapUsage(raw?: ChatResponse["usage"]): TokenUsage {
506
514
  const input = raw?.prompt_tokens ?? 0;
507
515
  const output = raw?.completion_tokens ?? 0;
@@ -517,12 +525,14 @@ function mapUsage(raw?: ChatResponse["usage"]): TokenUsage {
517
525
  cacheWriteTokens: raw?.prompt_tokens_details?.cache_write_tokens ?? 0,
518
526
  thinkingTokens: raw?.completion_tokens_details?.reasoning_tokens ?? 0,
519
527
  };
528
+ // `cost` is an OpenRouter extension. A generic endpoint that sends it may use 0 for "not computed", so only a positive value counts as known (OpenRouter's adapter accepts an explicit 0).
520
529
  if (typeof raw?.cost === "number" && raw.cost > 0) {
521
530
  usage.cost = { totalCost: raw.cost };
522
531
  }
523
532
  return usage;
524
533
  }
525
534
 
535
+ // The Gemini thought signature a proxy put on a tool call, if any; in: parseResponse, out: a string or undefined.
526
536
  function extractSignature(rawTc: {
527
537
  extra_content?: { google?: { thought_signature?: string } };
528
538
  }): string | undefined {
@@ -530,12 +540,13 @@ function extractSignature(rawTc: {
530
540
  return typeof sig === "string" && sig ? sig : undefined;
531
541
  }
532
542
 
533
- /** Tags a captured compat signature with its issuing provider (`google`, Q-18). */
543
+ /** Tags a captured compat signature with its issuing provider (`google`). */
534
544
  function withCompatSignature(sig: string | undefined): { thoughtSignature?: string; thoughtSignatureProvider?: string } {
535
545
  if (!sig) return {};
536
546
  return { thoughtSignature: sig, thoughtSignatureProvider: "google" };
537
547
  }
538
548
 
549
+ // Throws a concise error for an HTTP 200 reply that carries only `error`; in: parseResponse, out: a thrown error.
539
550
  function throwResponseError(
540
551
  response: ChatResponse,
541
552
  prefix: string,
@@ -550,6 +561,7 @@ function throwResponseError(
550
561
  throw toConciseProviderError(failure, prefix, modelId);
551
562
  }
552
563
 
564
+ // Turns a Chat Completions JSON reply into text, thinking, tool calls and usage; in: generate, out: ProviderGenerateResult.
553
565
  function parseResponse(
554
566
  response: ChatResponse,
555
567
  prefix: string,
@@ -563,7 +575,7 @@ function parseResponse(
563
575
 
564
576
  const choice = response.choices?.[0];
565
577
  const message = choice?.message;
566
- // Q-23: refusal content surfaces as text with a `refusal` finish reason.
578
+ // Refusal content surfaces as text with a `refusal` finish reason.
567
579
  const refusal = typeof message?.refusal === "string" && message.refusal ? message.refusal : "";
568
580
  const text = typeof message?.content === "string" && message.content ? message.content : refusal;
569
581
  // Thinking tolerance: plain `reasoning` plus per-block texts (deepseek-style
@@ -664,6 +676,7 @@ export class OpenAICompatibleChatProvider implements Provider {
664
676
  this.configuredApiKey = opts?.apiKey;
665
677
  }
666
678
 
679
+ // The catalog spec of a model, else a permissive generic one; in: the registry, out: a ModelSpec.
667
680
  getModel(modelId: string): ModelSpec | undefined {
668
681
  const clean = cleanModelId(this.prefix, modelId);
669
682
  return (
@@ -674,14 +687,17 @@ export class OpenAICompatibleChatProvider implements Provider {
674
687
  );
675
688
  }
676
689
 
690
+ // This provider's endpoint for one request; in: generate/stream, out: resolveBaseUrl.
677
691
  private baseUrl(options?: ProviderRequestOptions): string {
678
692
  return resolveBaseUrl(this.prefix, this.configuredBaseUrl, options);
679
693
  }
680
694
 
695
+ // This provider's key for one request; in: requireApiKey, out: resolveApiKey.
681
696
  private apiKey(options?: ProviderRequestOptions): string | undefined {
682
697
  return resolveApiKey(this.prefix, this.configuredApiKey, options);
683
698
  }
684
699
 
700
+ // The key, where a missing one only throws for non-local endpoints (loopback and LAN may omit it); in: generate/stream, out: the key or undefined.
685
701
  private requireApiKey(baseUrl: string, options?: ProviderRequestOptions): string | undefined {
686
702
  const key = this.apiKey(options);
687
703
  if (!key && !isLocalEndpoint(baseUrl)) {
@@ -692,6 +708,7 @@ export class OpenAICompatibleChatProvider implements Provider {
692
708
  return key;
693
709
  }
694
710
 
711
+ // Builds the Chat request (messages, tools, tool_choice, generation controls, response_format, stream usage) and warns+drops what a strict endpoint cannot take; in: generate/stream, out: the body.
695
712
  private async buildBody(
696
713
  modelId: string,
697
714
  context: ProviderContext,
@@ -772,7 +789,7 @@ export class OpenAICompatibleChatProvider implements Provider {
772
789
  // why: invalid output shapes fail fast at Agent construction/loop validation.
773
790
  }
774
791
  }
775
- // Q-38: request usage on streams where supported so terminal accounting
792
+ // Request usage on streams where supported so terminal accounting
776
793
  // arrives on the final chunk. Strict servers that reject unknown fields
777
794
  // opt out via `COMPAT_NO_STREAM_OPTIONS=1`; an explicit per-call
778
795
  // `(options as any).streamOptions === false` also skips it.
@@ -788,6 +805,7 @@ export class OpenAICompatibleChatProvider implements Provider {
788
805
  return body;
789
806
  }
790
807
 
808
+ // Fetch options with session-affinity headers (unless opted out), JSON content type and the Bearer key; in: doFetch and stream, out: RequestInit.
791
809
  private requestInit(
792
810
  body: ChatRequestBody,
793
811
  options: ProviderRequestOptions | undefined,
@@ -823,6 +841,7 @@ export class OpenAICompatibleChatProvider implements Provider {
823
841
  return { method: "POST", headers, body: JSON.stringify(body), signal };
824
842
  }
825
843
 
844
+ // One HTTP call under the request timeout, returning status, headers and body text; in: generate, out: the raw reply.
826
845
  private async doFetch(
827
846
  url: string,
828
847
  body: ChatRequestBody,
@@ -845,6 +864,7 @@ export class OpenAICompatibleChatProvider implements Provider {
845
864
  }
846
865
  }
847
866
 
867
+ // Throws a concise provider error (status, code, Retry-After, key hint) for a non-2xx reply; in: generate and stream, out: nothing or a thrown error.
848
868
  private throwIfError(
849
869
  status: number,
850
870
  url: string,
@@ -873,6 +893,7 @@ export class OpenAICompatibleChatProvider implements Provider {
873
893
  throw toConciseProviderError(err, this.prefix, modelId);
874
894
  }
875
895
 
896
+ // Non-streaming turn as one retried JSON request; in: runAgentLoop, out: ProviderGenerateResult.
876
897
  async generate(
877
898
  model: string | ModelSpec,
878
899
  context: ProviderContext,
@@ -945,6 +966,7 @@ export class OpenAICompatibleChatProvider implements Provider {
945
966
  }
946
967
  }
947
968
 
969
+ // Streaming turn over SSE: assembles text, reasoning, tool calls and usage, retrying only before the first byte; in: streamAgentLoop, out: an AssistantMessageEventStream.
948
970
  stream(
949
971
  model: string | ModelSpec,
950
972
  context: ProviderContext,
@@ -1046,7 +1068,7 @@ export class OpenAICompatibleChatProvider implements Provider {
1046
1068
  const choice = (Array.isArray(msg["choices"]) ? (msg["choices"] as Record<string, unknown>[])[0] : undefined) ?? {};
1047
1069
  const delta = (choice["delta"] as Record<string, unknown>) ?? {};
1048
1070
  // Text delta (content-free accounting frames carry "" — ignored).
1049
- // Q-23: refusal deltas surface as text with a `refusal` finish.
1071
+ // Refusal deltas surface as text with a `refusal` finish.
1050
1072
  const deltaRefusal = delta["refusal"];
1051
1073
  if (typeof deltaRefusal === "string" && deltaRefusal) {
1052
1074
  text += deltaRefusal;
@@ -1165,7 +1187,7 @@ export class OpenAICompatibleChatProvider implements Provider {
1165
1187
  sawFinishReason = true;
1166
1188
  }
1167
1189
 
1168
- // Q-23: terminal-state guard — fail `incomplete_stream` (retryable)
1190
+ // Terminal-state guard — fail `incomplete_stream` (retryable)
1169
1191
  // instead of resolving empty success. Skipped for non-SSE payloads
1170
1192
  // (JSON mocks) where no SSE frames were ever seen.
1171
1193
  const cleanThinking = thinking.replace(/\n{3,}/g, "\n\n").trim();
@@ -167,6 +167,7 @@ const DEFAULT_BASE_URL = "https://api.openai.com/v1";
167
167
  // Request building (canonical -> Responses)
168
168
  // ---------------------------------------------------------------------------
169
169
 
170
+ // Endpoint from the option, OPENAI_BASE_URL/OPENAI_API_BASE, else api.openai.com; in: each request, out: the base URL without a trailing slash.
170
171
  function resolveBaseUrl(options?: ProviderRequestOptions): string {
171
172
  return (
172
173
  options?.baseUrl ||
@@ -178,6 +179,7 @@ function resolveBaseUrl(options?: ProviderRequestOptions): string {
178
179
  ).replace(/\/+$/, "");
179
180
  }
180
181
 
182
+ // Key from the option or env, or `unused` when custom headers carry the auth; in: each request, out: the key or undefined.
181
183
  function resolveApiKey(options?: ProviderRequestOptions): string | undefined {
182
184
  const key = getApiKey("openai", options?.apiKey, options?.env);
183
185
  if (key) return key;
@@ -191,6 +193,7 @@ function cleanModelId(model: string | ModelSpec): string {
191
193
  return rawId.replace(/^openai\//i, "");
192
194
  }
193
195
 
196
+ // Canonical tool declarations as Responses function tools; in: options.tools, out: request `tools`.
194
197
  function toOpenAITools(tools?: StandardToolDeclaration[]): Array<Record<string, unknown>> | undefined {
195
198
  if (!tools || tools.length === 0) return undefined;
196
199
  return tools.map((t) => ({
@@ -202,6 +205,7 @@ function toOpenAITools(tools?: StandardToolDeclaration[]): Array<Record<string,
202
205
  }));
203
206
  }
204
207
 
208
+ // Text and media parts as input_text, input_image and input_file blocks; web links pass through, the rest is inlined as data URLs; in: a user message's parts, out: fullHistoryInput.
205
209
  async function contentPartsToBlocks(parts: ContentPart[]): Promise<ResponseContentPart[]> {
206
210
  const blocks: ResponseContentPart[] = [];
207
211
  for (const part of parts) {
@@ -258,17 +262,20 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ResponseConte
258
262
  return blocks;
259
263
  }
260
264
 
265
+ // A tool result as the text of a function_call_output item; in: fullHistoryInput, out: a string.
261
266
  function resultToOutput(result: unknown): string {
262
267
  if (typeof result === "string") return result;
263
268
  return safeStringify(result);
264
269
  }
265
270
 
271
+ // Makes an id safe for the Responses wire (allowed characters, at most 64, no trailing underscores); in: fullHistoryInput, out: the id.
266
272
  function normalizeResponsesIdPart(part: string): string {
267
273
  const sanitized = String(part ?? "").replace(/[^a-zA-Z0-9_-]/g, "_");
268
274
  const truncated = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
269
275
  return truncated.replace(/_+$/, "") || "fc_unknown";
270
276
  }
271
277
 
278
+ // Gives a function-call item id the required `fc_` prefix; in: fullHistoryInput, out: the id.
272
279
  function normalizeResponsesItemId(itemId: string): string {
273
280
  let norm = normalizeResponsesIdPart(itemId);
274
281
  if (!norm.startsWith("fc_")) {
@@ -390,6 +397,7 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
390
397
  // Response mapping (Responses -> canonical)
391
398
  // ---------------------------------------------------------------------------
392
399
 
400
+ // Responses usage as TokenUsage, clamping cache reads to input; in: a response or the completed event, out: TokenUsage.
393
401
  function mapUsage(raw?: ResponsesObject["usage"]): TokenUsage {
394
402
  const input = raw?.input_tokens ?? 0;
395
403
  const output = raw?.output_tokens ?? 0;
@@ -406,6 +414,7 @@ function mapUsage(raw?: ResponsesObject["usage"]): TokenUsage {
406
414
  };
407
415
  }
408
416
 
417
+ // Turns a Responses object into text, reasoning, tool calls and usage, mapping refusals and incomplete to finish reasons and throwing on failed; in: generate, out: ProviderGenerateResult.
409
418
  function parseResponse(
410
419
  response: ResponsesObject,
411
420
  modelId: string,
@@ -463,7 +472,7 @@ function parseResponse(
463
472
  }
464
473
  }
465
474
 
466
- // Q-23: `incomplete` -> length (content-filter truncations surface as
475
+ // `incomplete` -> length (content-filter truncations surface as
467
476
  // refusal when refusal text is present).
468
477
  const incompleteReason = (response as { incomplete_details?: { reason?: string } }).incomplete_details?.reason;
469
478
  const finishReason =
@@ -509,6 +518,7 @@ export class OpenAIResponsesProvider implements Provider {
509
518
  return getModelsForProvider("openai");
510
519
  }
511
520
 
521
+ // The catalog spec of a model, else a permissive generic one; in: the registry, out: a ModelSpec.
512
522
  getModel(modelId: string): ModelSpec | undefined {
513
523
  const clean = cleanModelId(modelId);
514
524
  return (
@@ -519,6 +529,7 @@ export class OpenAIResponsesProvider implements Provider {
519
529
  );
520
530
  }
521
531
 
532
+ // Builds the stateless Responses request (full history, store:false, tools, reasoning, tier, text.format, cache key and retention); in: generate/stream, out: the body.
522
533
  private async buildBody(
523
534
  modelId: string,
524
535
  context: ProviderContext,
@@ -620,6 +631,7 @@ export class OpenAIResponsesProvider implements Provider {
620
631
  return body;
621
632
  }
622
633
 
634
+ // Fetch options with session-affinity headers, JSON content type and the Bearer key; in: doFetch and stream, out: RequestInit.
623
635
  private requestInit(
624
636
  body: ResponsesRequestBody,
625
637
  options: ProviderRequestOptions | undefined,
@@ -640,6 +652,7 @@ export class OpenAIResponsesProvider implements Provider {
640
652
  return { method: "POST", headers, body: JSON.stringify(body), signal };
641
653
  }
642
654
 
655
+ // One HTTP call under the request timeout, returning status, headers and body text; in: generate, out: the raw reply.
643
656
  private async doFetch(
644
657
  url: string,
645
658
  body: ResponsesRequestBody,
@@ -662,6 +675,7 @@ export class OpenAIResponsesProvider implements Provider {
662
675
  }
663
676
  }
664
677
 
678
+ // Throws a concise provider error (status, code, Retry-After, key hint) for a non-2xx reply; in: generate and stream, out: nothing or a thrown error.
665
679
  private throwIfError(
666
680
  status: number,
667
681
  url: string,
@@ -692,6 +706,7 @@ export class OpenAIResponsesProvider implements Provider {
692
706
  throw toConciseProviderError(err, "openai", modelId);
693
707
  }
694
708
 
709
+ // Non-streaming turn as one retried JSON request; in: runAgentLoop, out: ProviderGenerateResult.
695
710
  async generate(
696
711
  model: string | ModelSpec,
697
712
  context: ProviderContext,
@@ -773,6 +788,7 @@ export class OpenAIResponsesProvider implements Provider {
773
788
  }
774
789
  }
775
790
 
791
+ // Streaming turn over SSE: assembles text, reasoning, tool calls and usage, retrying only before the first byte; in: streamAgentLoop, out: an AssistantMessageEventStream.
776
792
  stream(
777
793
  model: string | ModelSpec,
778
794
  context: ProviderContext,
@@ -793,7 +809,7 @@ export class OpenAIResponsesProvider implements Provider {
793
809
  );
794
810
  }
795
811
  assertModalitiesSupported(context, "openai", clean);
796
- assertNoVideoPartsOnResponses(context, "openai", clean);
812
+ assertNoVideoPartsOnResponses(context, "openai", clean);
797
813
  const baseUrl = resolveBaseUrl(options);
798
814
  const url = `${baseUrl}/responses`;
799
815
  const sessionId = options?.sessionId;
@@ -812,7 +828,7 @@ export class OpenAIResponsesProvider implements Provider {
812
828
  body,
813
829
  };
814
830
 
815
- // Q-17: optional per-call timeout races the linked abort signal.
831
+ // Optional per-call timeout races the linked abort signal.
816
832
  const streamTimeoutMs = timeoutFor(options as { timeoutMs?: number } | undefined);
817
833
  // Transient failures (429/5xx/network) are retried here, before the
818
834
  // first byte; once the stream is live it is never retried.
@@ -879,7 +895,7 @@ export class OpenAIResponsesProvider implements Provider {
879
895
  type === "response.refusal.delta" ||
880
896
  type === "response.output_refusal.delta"
881
897
  ) {
882
- // Q-23: refusal deltas carry the filter text; surface as text and
898
+ // Refusal deltas carry the filter text; surface as text and
883
899
  // remember for a `refusal` finish reason.
884
900
  if (typeof msg["delta"] === "string" && msg["delta"]) {
885
901
  text += msg["delta"] as string;
@@ -939,7 +955,7 @@ export class OpenAIResponsesProvider implements Provider {
939
955
  }
940
956
  // Non-streaming maps `incomplete` → `length` (max tokens). Streaming
941
957
  // must do the same — otherwise truncated turns misreport `stop`.
942
- // Q-23: content-filter incompletes surface as `refusal`.
958
+ // Content-filter incompletes surface as `refusal`.
943
959
  const incompleteReason = (response as { incomplete_details?: { reason?: string } }).incomplete_details?.reason;
944
960
  if (response.status === "incomplete") {
945
961
  finishReason = incompleteReason === "content_filter" ? "refusal" : "length";
@@ -1023,7 +1039,7 @@ export class OpenAIResponsesProvider implements Provider {
1023
1039
  sawFinishReason = true;
1024
1040
  }
1025
1041
 
1026
- // Q-23: terminal-state guard. Loops ending with no completed body, no
1042
+ // Terminal-state guard. Loops ending with no completed body, no
1027
1043
  // finish_reason, and no usage while carrying no content must fail as
1028
1044
  // `incomplete_stream` (retryable) instead of resolving empty success.
1029
1045
  // Skipped for non-SSE payloads (e.g. JSON mocks in delegation tests)
@@ -145,6 +145,7 @@ const DEFAULT_BASE_URL = "https://openrouter.ai/api/v1";
145
145
  // Request building (canonical -> Chat Completions)
146
146
  // ---------------------------------------------------------------------------
147
147
 
148
+ // Endpoint from the option, OPENROUTER_BASE_URL, else openrouter.ai; in: each request, out: the base URL without a trailing slash.
148
149
  function resolveBaseUrl(options?: ProviderRequestOptions): string {
149
150
  return (
150
151
  options?.baseUrl ||
@@ -154,6 +155,7 @@ function resolveBaseUrl(options?: ProviderRequestOptions): string {
154
155
  ).replace(/\/+$/, "");
155
156
  }
156
157
 
158
+ // Key from the option or env, or `unused` when custom headers carry the auth; in: each request, out: the key or undefined.
157
159
  function resolveApiKey(options?: ProviderRequestOptions): string | undefined {
158
160
  const key = getApiKey("openrouter", options?.apiKey, options?.env);
159
161
  if (key) return key;
@@ -172,11 +174,13 @@ function cleanModelId(model: string | ModelSpec): string {
172
174
 
173
175
  // 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.
174
176
  const RETRYABLE_ERROR_TYPE_STATUS: Record<string, number> = { rate_limit_exceeded: 429, provider_overloaded: 503, provider_unavailable: 502, timeout: 504, server: 500 };
177
+ // 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.
175
178
  function midStreamStatus(code: unknown, errorType: unknown): number | undefined {
176
179
  if (typeof code === "number" && Number.isInteger(code) && code >= 100 && code <= 599) return code;
177
180
  return typeof errorType === "string" ? RETRYABLE_ERROR_TYPE_STATUS[errorType] : undefined;
178
181
  }
179
182
 
183
+ // Canonical tool declarations as Chat Completions function tools; in: options.tools, out: request `tools`.
180
184
  function toOpenRouterTools(tools?: StandardToolDeclaration[]): Array<Record<string, unknown>> | undefined {
181
185
  if (!tools || tools.length === 0) return undefined;
182
186
  return tools.map((t) => ({
@@ -192,6 +196,7 @@ function toOpenRouterTools(tools?: StandardToolDeclaration[]): Array<Record<stri
192
196
 
193
197
  // 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.
194
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" };
199
+ // The `input_audio.format` for an audio MIME type (see AUDIO_FORMAT above); in: contentPartsToBlocks, out: the format string.
195
200
  function audioFormatFor(mimeType?: string): string {
196
201
  const subtype = (mimeType || "").toLowerCase().split(";")[0]!.split("/")[1]?.replace(/^x-/, "") ?? "";
197
202
  return AUDIO_FORMAT[subtype] ?? (subtype || "mp3");
@@ -278,6 +283,7 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ChatContentPa
278
283
  return blocks;
279
284
  }
280
285
 
286
+ // A tool result as the text of a tool message; in: fullHistoryMessages, out: a string.
281
287
  function resultToOutput(result: unknown): string {
282
288
  if (typeof result === "string") return result;
283
289
  return safeStringify(result);
@@ -392,6 +398,7 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
392
398
  // Response mapping (Chat Completions -> canonical)
393
399
  // ---------------------------------------------------------------------------
394
400
 
401
+ // Chat usage as TokenUsage with cache reads clamped to input, cache writes, reasoning tokens and OpenRouter's reported cost (0 counts); in: the last stream chunk, out: TokenUsage.
395
402
  function mapUsage(raw?: ChatUsage): TokenUsage {
396
403
  const input = raw?.prompt_tokens ?? 0;
397
404
  const output = raw?.completion_tokens ?? 0;
@@ -430,6 +437,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
430
437
  return getModelsForProvider("openrouter");
431
438
  }
432
439
 
440
+ // The catalog spec of a model, else a permissive generic one; in: the registry, out: a ModelSpec.
433
441
  getModel(modelId: string): ModelSpec | undefined {
434
442
  const clean = cleanModelId(modelId);
435
443
  return (
@@ -440,6 +448,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
440
448
  );
441
449
  }
442
450
 
451
+ // Builds the Chat request (messages, tools, tool_choice, reasoning, tier, cache_control, generation controls, response_format, session_id); in: streamInto, out: the body.
443
452
  private async buildBody(
444
453
  modelId: string,
445
454
  context: ProviderContext,
@@ -518,6 +527,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
518
527
  return body;
519
528
  }
520
529
 
530
+ // Fetch options with session and opt-in attribution headers, JSON content type and the Bearer key; in: streamInto, out: RequestInit.
521
531
  private requestInit(
522
532
  body: ChatRequestBody,
523
533
  options: ProviderRequestOptions | undefined,
@@ -537,6 +547,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
537
547
  return { method: "POST", headers, body: JSON.stringify(body), signal };
538
548
  }
539
549
 
550
+ // Throws a concise provider error (status, code, Retry-After, key and media-link hints) for a non-2xx reply, marking the in-flight-budget 402 retryable; in: streamInto, out: nothing or a thrown error.
540
551
  private throwIfError(
541
552
  status: number,
542
553
  url: string,
@@ -612,6 +623,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
612
623
  }
613
624
  }
614
625
 
626
+ // Streaming turn: a normal event stream from streamInto; in: streamAgentLoop, out: an AssistantMessageEventStream.
615
627
  stream(
616
628
  model: string | ModelSpec,
617
629
  context: ProviderContext,
@@ -729,7 +741,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
729
741
  const choice = (Array.isArray(msg["choices"]) ? (msg["choices"] as Record<string, unknown>[])[0] : undefined) ?? {};
730
742
  const delta = (choice["delta"] as Record<string, unknown>) ?? {};
731
743
  // Text delta (content-free accounting frames carry "" — ignored).
732
- // Q-23: refusal deltas surface as text with a `refusal` finish.
744
+ // Refusal deltas surface as text with a `refusal` finish.
733
745
  const deltaRefusal = delta["refusal"];
734
746
  if (typeof deltaRefusal === "string" && deltaRefusal) {
735
747
  text += deltaRefusal;
@@ -842,7 +854,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
842
854
  sawFinishReason = true;
843
855
  }
844
856
 
845
- // Q-23: terminal-state guard — fail `incomplete_stream` (retryable)
857
+ // Terminal-state guard — fail `incomplete_stream` (retryable)
846
858
  // instead of resolving empty success. Skipped for non-SSE payloads
847
859
  // (JSON mocks) where no SSE frames were ever seen.
848
860
  const cleanThinking = thinking.replace(/\n{3,}/g, "\n\n").trim();
@@ -42,6 +42,7 @@ const FIRST_CLASS = new Set([
42
42
  // resolve anywhere.
43
43
  const REMOVED_PROVIDERS = new Set(["opencode", "opencode-go"]);
44
44
 
45
+ // The error text for a removed provider prefix; in: assertProviderNotRemoved, out: a message.
45
46
  function removedProviderMessage(id: string): string {
46
47
  return (
47
48
  `[Agent Accelerator] Provider "${id}" has been removed: OpenCode free models are ` +
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Shared provider helpers (Q-45-partial).
2
+ * Shared provider helpers.
3
3
  *
4
4
  * Single home for the `redactedHeaders` / `readErrorPayload` /
5
5
  * `parseArguments` / `normalizeThinkingParts` helpers previously duplicated
@@ -78,7 +78,7 @@ export function readErrorPayload(
78
78
  }
79
79
  }
80
80
 
81
- /** Retryable incomplete-stream error for truncated SSE (Q-23). */
81
+ /** Retryable incomplete-stream error for truncated SSE. */
82
82
  export function incompleteStreamError(provider: string, modelId: string): Error {
83
83
  const err = toConciseProviderError(
84
84
  Object.assign(new Error("Stream ended without terminal event"), {
@@ -118,7 +118,7 @@ export function isNotFoundChainError(error: unknown): boolean {
118
118
  return code === "not_found";
119
119
  }
120
120
 
121
- /** Per-request timeout ms from provider options (Q-22). */
121
+ /** Per-request timeout ms from provider options. */
122
122
  export function timeoutFor(options: { timeoutMs?: number } | undefined): number | undefined {
123
123
  const t = (options as { timeoutMs?: unknown } | undefined)?.timeoutMs;
124
124
  return typeof t === "number" && Number.isFinite(t) && t > 0 ? Math.floor(t) : undefined;
@@ -149,7 +149,7 @@ export function combinedSignal(signal?: AbortSignal, timeoutMs?: number): { sign
149
149
  return { signal: ctrl.signal, cleanup };
150
150
  }
151
151
 
152
- /** Q-34: crypto-backed tool-call id, no Math.random primary. */
152
+ /** Crypto-backed tool-call id, no Math.random primary. */
153
153
  export function newToolCallId(prefix = "call"): string {
154
154
  try {
155
155
  const c = (globalThis as any)?.crypto;