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.
- package/README.md +10 -17
- package/package.json +1 -1
- package/src/agent/agent.ts +92 -57
- package/src/agent/architecture.md +25 -0
- package/src/agent/context.ts +7 -3
- package/src/agent/delegation.ts +30 -96
- package/src/agent/loop.ts +54 -56
- package/src/agent/subagent.ts +4 -4
- package/src/agent/worker.ts +1 -3
- package/src/architecture.md +28 -0
- package/src/index.ts +13 -80
- package/src/models/architecture.md +14 -0
- package/src/models/catalog-cache.ts +10 -7
- package/src/models/catalog.ts +20 -13
- package/src/providers/architecture.md +24 -0
- package/src/providers/google.ts +24 -4
- package/src/providers/openai-compat.ts +29 -7
- package/src/providers/openai.ts +22 -6
- package/src/providers/openrouter.ts +14 -2
- package/src/providers/registry.ts +1 -0
- package/src/providers/shared.ts +4 -4
- package/src/providers.ts +9 -53
- package/src/scheduler/architecture.md +22 -0
- package/src/scheduler/scheduler.ts +131 -56
- package/src/scheduler/store.ts +274 -54
- package/src/scheduler/time.ts +2 -0
- package/src/session/architecture.md +19 -0
- package/src/session/store.ts +37 -22
- package/src/session/usage.ts +1 -0
- package/src/streaming/architecture.md +13 -0
- package/src/streaming/event-stream.ts +16 -11
- package/src/telemetry-file.ts +74 -24
- package/src/telemetry.ts +3 -2
- package/src/tools/architecture.md +19 -0
- package/src/tools/executor.ts +14 -1
- package/src/tools/schema.ts +1 -0
- package/src/tools/subagent-result.ts +1 -0
- package/src/tools/tool.ts +8 -8
- package/src/types/agent.ts +22 -20
- package/src/types/architecture.md +20 -0
- package/src/types/response.ts +8 -8
- package/src/types/scheduler.ts +2 -2
- package/src/types/tool.ts +5 -5
- package/src/update-models.ts +2 -2
- package/src/utils/architecture.md +25 -0
- package/src/utils/base64.ts +2 -0
- package/src/utils/cache.ts +6 -4
- package/src/utils/durable.ts +78 -3
- package/src/utils/env.ts +1 -1
- package/src/utils/errors.ts +4 -0
- package/src/utils/headers.ts +7 -4
- package/src/utils/home.ts +1 -1
- package/src/utils/media.ts +1 -0
- package/src/utils/raw-capture.ts +3 -0
- package/src/utils/retry.ts +2 -1
- package/src/utils/safe-io.ts +4 -0
- 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
|
-
*
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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();
|
package/src/providers/openai.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 ` +
|
package/src/providers/shared.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared provider helpers
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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;
|