agent-accelerator 0.4.5 → 0.5.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.
- package/README.md +149 -57
- package/bunfig.toml +1 -0
- package/package.json +2 -3
- package/src/agent/agent.ts +401 -120
- package/src/agent/context.ts +11 -16
- package/src/agent/delegation.ts +111 -274
- package/src/agent/loop.ts +253 -98
- package/src/agent/subagent.ts +4 -12
- package/src/agent/worker.ts +435 -0
- package/src/index.ts +39 -5
- package/src/models/catalog-cache.ts +65 -25
- package/src/models/catalog.ts +21 -2
- package/src/providers/google.ts +282 -74
- package/src/providers/openai-compat.ts +61 -41
- package/src/providers/openai.ts +46 -31
- package/src/providers/openrouter.ts +42 -31
- package/src/providers/shared.ts +110 -16
- package/src/providers.ts +119 -6
- package/src/scheduler/scheduled-actions.md +41 -0
- package/src/scheduler/scheduler.ts +814 -0
- package/src/scheduler/store.ts +263 -0
- package/src/scheduler/time.ts +148 -0
- package/src/scheduler/tool.ts +151 -0
- package/src/session/store.ts +211 -104
- package/src/streaming/event-stream.ts +6 -4
- package/src/telemetry-file.ts +31 -0
- package/src/telemetry.ts +65 -0
- package/src/tools/executor.ts +163 -38
- package/src/tools/subagent-result.ts +60 -0
- package/src/types/agent.ts +57 -20
- package/src/types/model.ts +18 -0
- package/src/types/response.ts +2 -0
- package/src/types/scheduler.ts +81 -0
- package/src/types/tool.ts +15 -0
- package/src/utils/cache.ts +14 -5
- package/src/utils/documents.ts +112 -52
- package/src/utils/errors.ts +11 -0
- package/src/utils/headers.ts +15 -15
- package/src/utils/home.ts +30 -0
- package/src/utils/media.ts +57 -12
- package/src/utils/raw-capture.ts +76 -0
- package/src/utils/retry.ts +11 -3
- package/src/utils/safe-io.ts +252 -0
- package/src/utils/session.ts +7 -8
- package/src/data/README.md +0 -84
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Strictness contract (differs from the old lenient passthrough on purpose):
|
|
14
14
|
* - Requests carry ONLY standard Chat Completions fields (`model`, `messages`,
|
|
15
|
-
* `tools`, `tool_choice`, `stream
|
|
15
|
+
* `tools`, `tool_choice`, `stream`, plus `max_tokens`/
|
|
16
|
+
* `stop`/`seed` when explicitly set). No `reasoning`, `service_tier`,
|
|
16
17
|
* `session_id`, `prompt_cache_key`, or other extras: strict endpoints
|
|
17
18
|
* (groq, ollama, …) fail unknown body properties, so anything unsupported
|
|
18
19
|
* is warn+drop, never sent.
|
|
@@ -60,6 +61,7 @@ import { normalizeMediaInput } from "../utils/media.ts";
|
|
|
60
61
|
import { safeStringify } from "../utils/serialization.ts";
|
|
61
62
|
import { toConciseProviderError, assertModalitiesSupported } from "../utils/errors.ts";
|
|
62
63
|
import { withRetries } from "../utils/retry.ts";
|
|
64
|
+
import { ConfigError } from "../types/errors.ts";
|
|
63
65
|
import {
|
|
64
66
|
parseArguments,
|
|
65
67
|
normalizeThinkingParts,
|
|
@@ -72,6 +74,10 @@ import {
|
|
|
72
74
|
timeoutFor,
|
|
73
75
|
hasAuthHeader,
|
|
74
76
|
clampToolCallId,
|
|
77
|
+
openStreamResponse,
|
|
78
|
+
retryOptionsFrom,
|
|
79
|
+
headersToRecord,
|
|
80
|
+
parseRetryAfterMs,
|
|
75
81
|
} from "./shared.ts";
|
|
76
82
|
import { getModelFromCatalog, createGenericModelSpec, modelSupportsStructuredOutput } from "../models/catalog.ts";
|
|
77
83
|
import {
|
|
@@ -80,7 +86,9 @@ import {
|
|
|
80
86
|
emitProviderWarning,
|
|
81
87
|
noteProviderTurn,
|
|
82
88
|
parseStreamedToolArguments,
|
|
89
|
+
selectRawArguments,
|
|
83
90
|
mapOutputToCompatChat,
|
|
91
|
+
mapGenerationParamsToChat,
|
|
84
92
|
normalizeStructuredOutput,
|
|
85
93
|
augmentSystemPromptWithStructuredOutput,
|
|
86
94
|
} from "../providers.ts";
|
|
@@ -174,8 +182,6 @@ interface ChatResponse {
|
|
|
174
182
|
[k: string]: unknown;
|
|
175
183
|
}
|
|
176
184
|
|
|
177
|
-
const DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
178
|
-
|
|
179
185
|
/** Headers that must never leak onto native REST requests. */
|
|
180
186
|
const INTERNAL_HEADERS = new Set([
|
|
181
187
|
"x-thought-signature-map",
|
|
@@ -197,7 +203,7 @@ function resolveBaseUrl(
|
|
|
197
203
|
options?: ProviderRequestOptions
|
|
198
204
|
): string {
|
|
199
205
|
const envPrefix = envPrefixOf(prefix);
|
|
200
|
-
|
|
206
|
+
const resolved =
|
|
201
207
|
options?.baseUrl ||
|
|
202
208
|
configuredBaseUrl ||
|
|
203
209
|
options?.env?.[`${envPrefix}_BASE_URL`] ||
|
|
@@ -205,9 +211,15 @@ function resolveBaseUrl(
|
|
|
205
211
|
options?.env?.[`${envPrefix}_API_BASE`] ||
|
|
206
212
|
getEnv(`${envPrefix}_BASE_URL`) ||
|
|
207
213
|
getEnv(`${envPrefix}_BASEURL`) ||
|
|
208
|
-
getEnv(`${envPrefix}_API_BASE`)
|
|
209
|
-
|
|
210
|
-
|
|
214
|
+
getEnv(`${envPrefix}_API_BASE`);
|
|
215
|
+
// Never fall back to another vendor's cloud: a custom prefix's API key
|
|
216
|
+
// must only ever travel to the endpoint the developer named.
|
|
217
|
+
if (!resolved) {
|
|
218
|
+
throw new ConfigError(
|
|
219
|
+
`[Agent Accelerator] Missing base URL for "${prefix}". Set ${envPrefix}_BASE_URL (or pass baseUrl); custom endpoints never default to api.openai.com.`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return resolved.replace(/\/+$/, "");
|
|
211
223
|
}
|
|
212
224
|
|
|
213
225
|
function resolveApiKey(
|
|
@@ -612,7 +624,7 @@ export interface CustomProviderOptions {
|
|
|
612
624
|
baseUrl?: string;
|
|
613
625
|
/** Static apiKey — overrides env. Explicit per-call `apiKey` still wins. */
|
|
614
626
|
apiKey?: string;
|
|
615
|
-
/** Default baseUrl when no env is set.
|
|
627
|
+
/** Default baseUrl when no env is set. There is no implicit default: without one the request fails fast. */
|
|
616
628
|
defaultBaseUrl?: string;
|
|
617
629
|
}
|
|
618
630
|
|
|
@@ -733,6 +745,8 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
733
745
|
};
|
|
734
746
|
const compatTools = toCompatTools(tools);
|
|
735
747
|
if (compatTools) body.tools = compatTools;
|
|
748
|
+
// Standard Chat Completions generation fields, only when the caller set them.
|
|
749
|
+
Object.assign(body, mapGenerationParamsToChat(options));
|
|
736
750
|
const toolChoice = mapToolChoiceToOpenAI(
|
|
737
751
|
options?.toolChoice as "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
|
|
738
752
|
);
|
|
@@ -822,7 +836,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
822
836
|
): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
|
|
823
837
|
const { signal: effective, cleanup } = combinedSignal(signal, timeoutFor(options as { timeoutMs?: number } | undefined));
|
|
824
838
|
try {
|
|
825
|
-
const res = await fetch(url, this.requestInit(body, options, apiKey, sessionId, effective));
|
|
839
|
+
const res = await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, effective));
|
|
826
840
|
const text = await res.text();
|
|
827
841
|
const headers: Record<string, string> = {};
|
|
828
842
|
res.headers.forEach((v, k) => {
|
|
@@ -838,7 +852,8 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
838
852
|
status: number,
|
|
839
853
|
url: string,
|
|
840
854
|
bodyText: string,
|
|
841
|
-
modelId: string
|
|
855
|
+
modelId: string,
|
|
856
|
+
headers?: Record<string, string>
|
|
842
857
|
): void {
|
|
843
858
|
if (status >= 200 && status < 300) return;
|
|
844
859
|
const { message, code, errorType, raw } = readErrorPayload(bodyText);
|
|
@@ -856,6 +871,8 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
856
871
|
if (code !== undefined) (err as Record<string, unknown>)["code"] = code;
|
|
857
872
|
if (errorType) (err as Record<string, unknown>)["errorType"] = errorType;
|
|
858
873
|
if (raw) (err as Record<string, unknown>)["raw"] = raw;
|
|
874
|
+
const retryAfterMs = parseRetryAfterMs(headers);
|
|
875
|
+
if (retryAfterMs !== undefined) (err as Record<string, unknown>)["retryAfterMs"] = retryAfterMs;
|
|
859
876
|
throw toConciseProviderError(err, this.prefix, modelId);
|
|
860
877
|
}
|
|
861
878
|
|
|
@@ -866,13 +883,13 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
866
883
|
): Promise<ProviderGenerateResult> {
|
|
867
884
|
const startTime = Date.now();
|
|
868
885
|
const clean = cleanModelId(this.prefix, model);
|
|
869
|
-
const baseUrl = this.baseUrl(options);
|
|
870
|
-
const apiKey = this.requireApiKey(baseUrl, options);
|
|
871
886
|
// Fail fast before any network call when the catalog knows the model
|
|
872
887
|
// lacks the requested modality. Unknown models skip the guard; the
|
|
873
|
-
// endpoint verdict surfaces concisely.
|
|
888
|
+
// endpoint verdict surfaces concisely. Cheap local checks run first.
|
|
874
889
|
assertModalitiesSupported(context, this.prefix, clean);
|
|
875
890
|
assertNoVideoOrFileParts(context, this.prefix, clean);
|
|
891
|
+
const baseUrl = this.baseUrl(options);
|
|
892
|
+
const apiKey = this.requireApiKey(baseUrl, options);
|
|
876
893
|
const url = `${baseUrl}/chat/completions`;
|
|
877
894
|
const sessionId = options?.sessionId || options?.cache?.sessionId;
|
|
878
895
|
const tools = options?.tools as StandardToolDeclaration[] | undefined;
|
|
@@ -895,7 +912,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
895
912
|
|
|
896
913
|
const doCall = async (): Promise<ProviderGenerateResult> => {
|
|
897
914
|
const res = await this.doFetch(url, body, options, apiKey, sessionId, options?.signal);
|
|
898
|
-
this.throwIfError(res.status, url, res.text, clean);
|
|
915
|
+
this.throwIfError(res.status, url, res.text, clean, res.headers);
|
|
899
916
|
let response: ChatResponse;
|
|
900
917
|
try {
|
|
901
918
|
response = JSON.parse(res.text) as ChatResponse;
|
|
@@ -924,8 +941,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
924
941
|
|
|
925
942
|
try {
|
|
926
943
|
return await withRetries(doCall, {
|
|
927
|
-
|
|
928
|
-
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
944
|
+
...retryOptionsFrom(options),
|
|
929
945
|
signal: options?.signal,
|
|
930
946
|
label: { providerId: this.prefix, modelId: clean },
|
|
931
947
|
});
|
|
@@ -965,10 +981,10 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
965
981
|
|
|
966
982
|
(async () => {
|
|
967
983
|
try {
|
|
968
|
-
const baseUrl = this.baseUrl(options);
|
|
969
|
-
const apiKey = this.requireApiKey(baseUrl, options);
|
|
970
984
|
assertModalitiesSupported(context, prefix, clean);
|
|
971
985
|
assertNoVideoOrFileParts(context, prefix, clean);
|
|
986
|
+
const baseUrl = this.baseUrl(options);
|
|
987
|
+
const apiKey = this.requireApiKey(baseUrl, options);
|
|
972
988
|
const url = `${baseUrl}/chat/completions`;
|
|
973
989
|
const sessionId = options?.sessionId || options?.cache?.sessionId;
|
|
974
990
|
const tools = options?.tools as StandardToolDeclaration[] | undefined;
|
|
@@ -990,25 +1006,30 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
990
1006
|
};
|
|
991
1007
|
|
|
992
1008
|
const streamTimeoutMs = timeoutFor(options as { timeoutMs?: number } | undefined);
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1009
|
+
// Transient failures (429/5xx/network) are retried here, before the
|
|
1010
|
+
// first byte; once the stream is live it is never retried.
|
|
1011
|
+
const res = await openStreamResponse({
|
|
1012
|
+
fetchOnce: async () => {
|
|
1013
|
+
const timer =
|
|
1014
|
+
streamTimeoutMs && streamTimeoutMs > 0
|
|
1015
|
+
? setTimeout(() => {
|
|
1016
|
+
try {
|
|
1017
|
+
linked.abort();
|
|
1018
|
+
} catch {}
|
|
1019
|
+
}, streamTimeoutMs)
|
|
1020
|
+
: undefined;
|
|
1021
|
+
try {
|
|
1022
|
+
return await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal));
|
|
1023
|
+
} finally {
|
|
1024
|
+
if (timer) clearTimeout(timer);
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
onHttpError: (r, text) => this.throwIfError(r.status, url, text, clean, headersToRecord(r.headers)),
|
|
1028
|
+
noBodyError: () => toConciseProviderError(new Error("OpenAI-compatible streaming response had no body"), prefix, clean),
|
|
1029
|
+
retry: retryOptionsFrom(options),
|
|
1030
|
+
signal: linked.signal,
|
|
1031
|
+
label: { providerId: prefix, modelId: clean },
|
|
1032
|
+
});
|
|
1012
1033
|
const responseHeaders: Record<string, string> = {};
|
|
1013
1034
|
res.headers.forEach((v, k) => {
|
|
1014
1035
|
responseHeaders[k] = v;
|
|
@@ -1017,7 +1038,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1017
1038
|
eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
|
|
1018
1039
|
|
|
1019
1040
|
const parser = new SSEParser();
|
|
1020
|
-
const reader = res.body
|
|
1041
|
+
const reader = res.body!.getReader(); // openStreamResponse guarantees a body
|
|
1021
1042
|
const decoder = new TextDecoder();
|
|
1022
1043
|
let text = "";
|
|
1023
1044
|
let thinking = "";
|
|
@@ -1199,7 +1220,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1199
1220
|
id: c.id && !c.id.includes("|") && c.id.length <= 40 ? c.id : clampToolCallId(c.id || newToolCallId()),
|
|
1200
1221
|
name: c.name,
|
|
1201
1222
|
arguments: args,
|
|
1202
|
-
rawArguments: c.startArgs
|
|
1223
|
+
rawArguments: selectRawArguments(c.startArgs, c.deltaArgs),
|
|
1203
1224
|
...withCompatSignature(sig),
|
|
1204
1225
|
} as ToolCallRecord;
|
|
1205
1226
|
toolCalls.push(record);
|
|
@@ -1262,8 +1283,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1262
1283
|
const isAbort =
|
|
1263
1284
|
linked.signal.aborted ||
|
|
1264
1285
|
eventStream.isCancelled() ||
|
|
1265
|
-
(raw as { name?: string }).name === "AbortError"
|
|
1266
|
-
/abort|cancell?ed/i.test(String((raw as { message?: string }).message ?? raw));
|
|
1286
|
+
(raw as { name?: string }).name === "AbortError";
|
|
1267
1287
|
eventStream.fail(
|
|
1268
1288
|
isAbort ? Object.assign(new Error("Stream aborted"), { name: "AbortError" }) : raw
|
|
1269
1289
|
);
|
package/src/providers/openai.ts
CHANGED
|
@@ -12,10 +12,11 @@
|
|
|
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).
|
|
16
|
+
*
|
|
15
17
|
* Out of scope (never sent): background, conversation, previous_response_id
|
|
16
|
-
* chaining, include, metadata,
|
|
17
|
-
*
|
|
18
|
-
* text.format structured output, built-in/MCP tools. Those remain
|
|
18
|
+
* chaining, include, metadata, truncation, safety_identifier,
|
|
19
|
+
* prompt_cache_options explicit breakpoints, built-in/MCP tools. Those remain
|
|
19
20
|
* UNSUPPORTED and are handled via warn+drop in the canonical layer where
|
|
20
21
|
* applicable.
|
|
21
22
|
*
|
|
@@ -64,6 +65,10 @@ import {
|
|
|
64
65
|
timeoutFor,
|
|
65
66
|
hasAuthHeader,
|
|
66
67
|
clampToolCallId,
|
|
68
|
+
openStreamResponse,
|
|
69
|
+
retryOptionsFrom,
|
|
70
|
+
headersToRecord,
|
|
71
|
+
parseRetryAfterMs,
|
|
67
72
|
} from "./shared.ts";
|
|
68
73
|
import { createGenericModelSpec } from "../models/catalog.ts";
|
|
69
74
|
import { getModelFromCatalog, getModelsForProvider, modelSupportsStructuredOutput } from "../models/catalog.ts";
|
|
@@ -74,7 +79,9 @@ import {
|
|
|
74
79
|
mapToolChoiceToOpenAI,
|
|
75
80
|
noteProviderTurn,
|
|
76
81
|
parseStreamedToolArguments,
|
|
82
|
+
selectRawArguments,
|
|
77
83
|
mapOutputToOpenAI,
|
|
84
|
+
mapGenerationParamsToOpenAI,
|
|
78
85
|
normalizeStructuredOutput,
|
|
79
86
|
augmentSystemPromptWithStructuredOutput,
|
|
80
87
|
emitProviderWarning,
|
|
@@ -570,6 +577,8 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
570
577
|
if (toolChoice !== undefined) body.tool_choice = toolChoice;
|
|
571
578
|
if (effort) body.reasoning = { effort };
|
|
572
579
|
if (serviceTier) body.service_tier = serviceTier;
|
|
580
|
+
// max_output_tokens (stop + seed warn+drop: not on this wire).
|
|
581
|
+
Object.assign(body, mapGenerationParamsToOpenAI(options, `openai/${modelId}`));
|
|
573
582
|
// Structured outputs: Responses `text.format{type:json_schema,name,schema,strict}`.
|
|
574
583
|
// Skipped when emulated (instruction already in `instructions`).
|
|
575
584
|
if (outputSpec && !outputEmulated) {
|
|
@@ -651,7 +660,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
651
660
|
): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
|
|
652
661
|
const { signal: effective, cleanup } = combinedSignal(signal, timeoutFor(options as { timeoutMs?: number } | undefined));
|
|
653
662
|
try {
|
|
654
|
-
const res = await fetch(url, this.requestInit(body, options, apiKey, sessionId, effective));
|
|
663
|
+
const res = await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, effective));
|
|
655
664
|
const text = await res.text();
|
|
656
665
|
const headers: Record<string, string> = {};
|
|
657
666
|
res.headers.forEach((v, k) => {
|
|
@@ -667,7 +676,8 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
667
676
|
status: number,
|
|
668
677
|
url: string,
|
|
669
678
|
bodyText: string,
|
|
670
|
-
modelId: string
|
|
679
|
+
modelId: string,
|
|
680
|
+
headers?: Record<string, string>
|
|
671
681
|
): void {
|
|
672
682
|
if (status >= 200 && status < 300) return;
|
|
673
683
|
const { message, code, errorType, raw } = readErrorPayload(bodyText);
|
|
@@ -687,6 +697,8 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
687
697
|
if (code !== undefined) (err as Record<string, unknown>)["code"] = code;
|
|
688
698
|
if (errorType) (err as Record<string, unknown>)["errorType"] = errorType;
|
|
689
699
|
if (raw) (err as Record<string, unknown>)["raw"] = raw;
|
|
700
|
+
const retryAfterMs = parseRetryAfterMs(headers);
|
|
701
|
+
if (retryAfterMs !== undefined) (err as Record<string, unknown>)["retryAfterMs"] = retryAfterMs;
|
|
690
702
|
throw toConciseProviderError(err, "openai", modelId);
|
|
691
703
|
}
|
|
692
704
|
|
|
@@ -736,7 +748,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
736
748
|
|
|
737
749
|
const doCall = async (): Promise<ProviderGenerateResult> => {
|
|
738
750
|
const res = await this.doFetch(url, body, options, apiKey, sessionId, options?.signal);
|
|
739
|
-
this.throwIfError(res.status, url, res.text, clean);
|
|
751
|
+
this.throwIfError(res.status, url, res.text, clean, res.headers);
|
|
740
752
|
let response: ResponsesObject;
|
|
741
753
|
try {
|
|
742
754
|
response = JSON.parse(res.text) as ResponsesObject;
|
|
@@ -764,8 +776,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
764
776
|
|
|
765
777
|
try {
|
|
766
778
|
return await withRetries(doCall, {
|
|
767
|
-
|
|
768
|
-
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
779
|
+
...retryOptionsFrom(options),
|
|
769
780
|
signal: options?.signal,
|
|
770
781
|
label: { providerId: "openai", modelId: clean },
|
|
771
782
|
});
|
|
@@ -835,25 +846,30 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
835
846
|
|
|
836
847
|
// Q-17: optional per-call timeout races the linked abort signal.
|
|
837
848
|
const streamTimeoutMs = timeoutFor(options as { timeoutMs?: number } | undefined);
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
849
|
+
// Transient failures (429/5xx/network) are retried here, before the
|
|
850
|
+
// first byte; once the stream is live it is never retried.
|
|
851
|
+
const res = await openStreamResponse({
|
|
852
|
+
fetchOnce: async () => {
|
|
853
|
+
const timer =
|
|
854
|
+
streamTimeoutMs && streamTimeoutMs > 0
|
|
855
|
+
? setTimeout(() => {
|
|
856
|
+
try {
|
|
857
|
+
linked.abort();
|
|
858
|
+
} catch {}
|
|
859
|
+
}, streamTimeoutMs)
|
|
860
|
+
: undefined;
|
|
861
|
+
try {
|
|
862
|
+
return await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal));
|
|
863
|
+
} finally {
|
|
864
|
+
if (timer) clearTimeout(timer);
|
|
865
|
+
}
|
|
866
|
+
},
|
|
867
|
+
onHttpError: (r, text) => this.throwIfError(r.status, url, text, clean, headersToRecord(r.headers)),
|
|
868
|
+
noBodyError: () => toConciseProviderError(new Error("OpenAI streaming response had no body"), "openai", clean),
|
|
869
|
+
retry: retryOptionsFrom(options),
|
|
870
|
+
signal: linked.signal,
|
|
871
|
+
label: { providerId: "openai", modelId: clean },
|
|
872
|
+
});
|
|
857
873
|
const responseHeaders: Record<string, string> = {};
|
|
858
874
|
res.headers.forEach((v, k) => {
|
|
859
875
|
responseHeaders[k] = v;
|
|
@@ -862,7 +878,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
862
878
|
eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
|
|
863
879
|
|
|
864
880
|
const parser = new SSEParser();
|
|
865
|
-
const reader = res.body
|
|
881
|
+
const reader = res.body!.getReader(); // openStreamResponse guarantees a body
|
|
866
882
|
const decoder = new TextDecoder();
|
|
867
883
|
let text = "";
|
|
868
884
|
let thinking = "";
|
|
@@ -1070,7 +1086,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
1070
1086
|
callId: c.callId || undefined,
|
|
1071
1087
|
name: c.name,
|
|
1072
1088
|
arguments: args,
|
|
1073
|
-
rawArguments: c.startArgs
|
|
1089
|
+
rawArguments: selectRawArguments(c.startArgs, c.deltaArgs),
|
|
1074
1090
|
};
|
|
1075
1091
|
toolCalls.push(record);
|
|
1076
1092
|
eventStream.push({ type: "tool_call_complete", toolCall: record });
|
|
@@ -1134,8 +1150,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
1134
1150
|
const isAbort =
|
|
1135
1151
|
linked.signal.aborted ||
|
|
1136
1152
|
eventStream.isCancelled() ||
|
|
1137
|
-
(raw as { name?: string }).name === "AbortError"
|
|
1138
|
-
/abort|cancell?ed/i.test(String((raw as { message?: string }).message ?? raw));
|
|
1153
|
+
(raw as { name?: string }).name === "AbortError";
|
|
1139
1154
|
eventStream.fail(
|
|
1140
1155
|
isAbort ? Object.assign(new Error("Stream aborted"), { name: "AbortError" }) : raw
|
|
1141
1156
|
);
|
|
@@ -57,6 +57,10 @@ import {
|
|
|
57
57
|
timeoutFor,
|
|
58
58
|
hasAuthHeader,
|
|
59
59
|
clampToolCallId,
|
|
60
|
+
openStreamResponse,
|
|
61
|
+
retryOptionsFrom,
|
|
62
|
+
headersToRecord,
|
|
63
|
+
parseRetryAfterMs,
|
|
60
64
|
} from "./shared.ts";
|
|
61
65
|
import { createGenericModelSpec } from "../models/catalog.ts";
|
|
62
66
|
import { getModelFromCatalog, getModelsForProvider, modelSupportsStructuredOutput } from "../models/catalog.ts";
|
|
@@ -67,7 +71,9 @@ import {
|
|
|
67
71
|
mapToolChoiceToOpenRouterChat,
|
|
68
72
|
noteProviderTurn,
|
|
69
73
|
parseStreamedToolArguments,
|
|
74
|
+
selectRawArguments,
|
|
70
75
|
mapOutputToOpenRouterChat,
|
|
76
|
+
mapGenerationParamsToChat,
|
|
71
77
|
normalizeStructuredOutput,
|
|
72
78
|
augmentSystemPromptWithStructuredOutput,
|
|
73
79
|
emitProviderWarning,
|
|
@@ -622,6 +628,8 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
622
628
|
if (toolChoice !== undefined) body.tool_choice = toolChoice;
|
|
623
629
|
if (effort) body.reasoning = { effort };
|
|
624
630
|
if (serviceTier) body.service_tier = serviceTier;
|
|
631
|
+
// Documented Chat Completions generation controls (only those the caller set).
|
|
632
|
+
Object.assign(body, mapGenerationParamsToChat(options));
|
|
625
633
|
// Structured outputs: Chat Completions `response_format{type:json_schema,json_schema}`.
|
|
626
634
|
// Skipped when emulated (unsupported model): instruction already in messages.
|
|
627
635
|
if (outputSpec && !outputEmulated) {
|
|
@@ -635,9 +643,6 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
635
643
|
// PDF ingestion is a documented plugin, not a message shape: enable it
|
|
636
644
|
// only when file parts are present (never by default).
|
|
637
645
|
if (hasFiles) body.plugins = [{ id: "file-parser" }];
|
|
638
|
-
// No temperature/top_p/max_tokens/stop/user/seed knobs
|
|
639
|
-
// (no canonical options exist; omitting also keeps provider cache keys
|
|
640
|
-
// stable). `response_format` above is the sole structured-output exception.
|
|
641
646
|
// No store/previous_response_id/conversation: they do not exist
|
|
642
647
|
// on this endpoint.
|
|
643
648
|
// Session affinity: top-level `session_id` is the documented sticky-routing
|
|
@@ -682,7 +687,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
682
687
|
): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
|
|
683
688
|
const { signal: effective, cleanup } = combinedSignal(signal, timeoutFor(options as { timeoutMs?: number } | undefined));
|
|
684
689
|
try {
|
|
685
|
-
const res = await fetch(url, this.requestInit(body, options, apiKey, sessionId, effective));
|
|
690
|
+
const res = await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, effective));
|
|
686
691
|
const text = await res.text();
|
|
687
692
|
const headers: Record<string, string> = {};
|
|
688
693
|
res.headers.forEach((v, k) => {
|
|
@@ -698,7 +703,8 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
698
703
|
status: number,
|
|
699
704
|
url: string,
|
|
700
705
|
bodyText: string,
|
|
701
|
-
modelId: string
|
|
706
|
+
modelId: string,
|
|
707
|
+
headers?: Record<string, string>
|
|
702
708
|
): void {
|
|
703
709
|
if (status >= 200 && status < 300) return;
|
|
704
710
|
const { message, code, errorType, raw } = readErrorPayload(bodyText);
|
|
@@ -716,6 +722,8 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
716
722
|
if (code !== undefined) (err as Record<string, unknown>)["code"] = code;
|
|
717
723
|
if (errorType) (err as Record<string, unknown>)["errorType"] = errorType;
|
|
718
724
|
if (raw) (err as Record<string, unknown>)["raw"] = raw;
|
|
725
|
+
const retryAfterMs = parseRetryAfterMs(headers);
|
|
726
|
+
if (retryAfterMs !== undefined) (err as Record<string, unknown>)["retryAfterMs"] = retryAfterMs;
|
|
719
727
|
throw toConciseProviderError(err, "openrouter", modelId);
|
|
720
728
|
}
|
|
721
729
|
|
|
@@ -762,7 +770,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
762
770
|
|
|
763
771
|
const doCall = async (): Promise<ProviderGenerateResult> => {
|
|
764
772
|
const res = await this.doFetch(url, body, options, apiKey, sessionId, options?.signal);
|
|
765
|
-
this.throwIfError(res.status, url, res.text, clean);
|
|
773
|
+
this.throwIfError(res.status, url, res.text, clean, res.headers);
|
|
766
774
|
let response: ChatResponse;
|
|
767
775
|
try {
|
|
768
776
|
response = JSON.parse(res.text) as ChatResponse;
|
|
@@ -790,8 +798,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
790
798
|
|
|
791
799
|
try {
|
|
792
800
|
return await withRetries(doCall, {
|
|
793
|
-
|
|
794
|
-
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
801
|
+
...retryOptionsFrom(options),
|
|
795
802
|
signal: options?.signal,
|
|
796
803
|
label: { providerId: "openrouter", modelId: clean },
|
|
797
804
|
});
|
|
@@ -859,25 +866,30 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
859
866
|
};
|
|
860
867
|
|
|
861
868
|
const streamTimeoutMs = timeoutFor(options as { timeoutMs?: number } | undefined);
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
869
|
+
// Transient failures (429/5xx/network) are retried here, before the
|
|
870
|
+
// first byte; once the stream is live it is never retried.
|
|
871
|
+
const res = await openStreamResponse({
|
|
872
|
+
fetchOnce: async () => {
|
|
873
|
+
const timer =
|
|
874
|
+
streamTimeoutMs && streamTimeoutMs > 0
|
|
875
|
+
? setTimeout(() => {
|
|
876
|
+
try {
|
|
877
|
+
linked.abort();
|
|
878
|
+
} catch {}
|
|
879
|
+
}, streamTimeoutMs)
|
|
880
|
+
: undefined;
|
|
881
|
+
try {
|
|
882
|
+
return await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal));
|
|
883
|
+
} finally {
|
|
884
|
+
if (timer) clearTimeout(timer);
|
|
885
|
+
}
|
|
886
|
+
},
|
|
887
|
+
onHttpError: (r, text) => this.throwIfError(r.status, url, text, clean, headersToRecord(r.headers)),
|
|
888
|
+
noBodyError: () => toConciseProviderError(new Error("OpenRouter streaming response had no body"), "openrouter", clean),
|
|
889
|
+
retry: retryOptionsFrom(options),
|
|
890
|
+
signal: linked.signal,
|
|
891
|
+
label: { providerId: "openrouter", modelId: clean },
|
|
892
|
+
});
|
|
881
893
|
const responseHeaders: Record<string, string> = {};
|
|
882
894
|
res.headers.forEach((v, k) => {
|
|
883
895
|
responseHeaders[k] = v;
|
|
@@ -886,7 +898,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
886
898
|
eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
|
|
887
899
|
|
|
888
900
|
const parser = new SSEParser();
|
|
889
|
-
const reader = res.body
|
|
901
|
+
const reader = res.body!.getReader(); // openStreamResponse guarantees a body
|
|
890
902
|
const decoder = new TextDecoder();
|
|
891
903
|
let text = "";
|
|
892
904
|
let thinking = "";
|
|
@@ -1065,7 +1077,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
1065
1077
|
id: c.id && !c.id.includes("|") && c.id.length <= 40 ? c.id : clampToolCallId(c.id || newToolCallId()),
|
|
1066
1078
|
name: c.name,
|
|
1067
1079
|
arguments: args,
|
|
1068
|
-
rawArguments: c.startArgs
|
|
1080
|
+
rawArguments: selectRawArguments(c.startArgs, c.deltaArgs),
|
|
1069
1081
|
};
|
|
1070
1082
|
toolCalls.push(record);
|
|
1071
1083
|
eventStream.push({ type: "tool_call_complete", toolCall: record });
|
|
@@ -1126,8 +1138,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
|
|
|
1126
1138
|
const isAbort =
|
|
1127
1139
|
linked.signal.aborted ||
|
|
1128
1140
|
eventStream.isCancelled() ||
|
|
1129
|
-
(raw as { name?: string }).name === "AbortError"
|
|
1130
|
-
/abort|cancell?ed/i.test(String((raw as { message?: string }).message ?? raw));
|
|
1141
|
+
(raw as { name?: string }).name === "AbortError";
|
|
1131
1142
|
eventStream.fail(
|
|
1132
1143
|
isAbort ? Object.assign(new Error("Stream aborted"), { name: "AbortError" }) : raw
|
|
1133
1144
|
);
|