agent-accelerator 0.5.8 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -5
- package/examples/prompts/SYSTEM_PROMPT.md +2 -0
- package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
- package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
- package/package.json +4 -1
- package/src/agent/agent.ts +374 -167
- package/src/agent/architecture.md +14 -12
- package/src/agent/config-blocks.ts +39 -12
- package/src/agent/context.ts +8 -3
- package/src/agent/delegation.ts +82 -29
- package/src/agent/loop.ts +449 -373
- package/src/agent/subagent.ts +1 -3
- package/src/agent/subagents-tool.ts +45 -7
- package/src/agent/subagents.md +3 -0
- package/src/agent/turns.ts +3 -1
- package/src/agent/worker.ts +58 -28
- package/src/agent/workers.ts +113 -20
- package/src/architecture.md +16 -15
- package/src/index.ts +34 -10
- package/src/models/architecture.md +4 -3
- package/src/models/catalog-cache.ts +6 -5
- package/src/models/catalog.ts +46 -49
- package/src/providers/architecture.md +10 -8
- package/src/providers/google.ts +150 -62
- package/src/providers/openai-compat.ts +60 -58
- package/src/providers/openai.ts +57 -39
- package/src/providers/openrouter.ts +87 -32
- package/src/providers/registry.ts +12 -26
- package/src/providers/shared.ts +89 -39
- package/src/providers.ts +56 -115
- package/src/scheduler/architecture.md +9 -8
- package/src/scheduler/scheduled-actions.md +4 -0
- package/src/scheduler/scheduler.ts +186 -60
- package/src/scheduler/store.ts +23 -10
- package/src/scheduler/time.ts +17 -3
- package/src/scheduler/tool.ts +33 -7
- package/src/session/architecture.md +7 -6
- package/src/session/journal.ts +38 -12
- package/src/session/store.ts +122 -61
- package/src/session/usage.ts +58 -9
- package/src/streaming/architecture.md +4 -3
- package/src/streaming/event-stream.ts +26 -12
- package/src/telemetry.ts +6 -2
- package/src/tools/architecture.md +8 -7
- package/src/tools/executor.ts +50 -48
- package/src/tools/schema.ts +10 -6
- package/src/tools/subagent-result.ts +6 -1
- package/src/tools/tool.ts +9 -11
- package/src/types/agent.ts +2 -21
- package/src/types/architecture.md +11 -10
- package/src/types/core.ts +1 -9
- package/src/types/errors.ts +26 -4
- package/src/types/model.ts +0 -4
- package/src/types/tool.ts +12 -2
- package/src/update-models.ts +5 -5
- package/src/utils/architecture.md +16 -15
- package/src/utils/cache.ts +1 -3
- package/src/utils/durable.ts +17 -5
- package/src/utils/env.ts +9 -7
- package/src/utils/errors.ts +50 -44
- package/src/utils/headers.ts +3 -10
- package/src/utils/media.ts +23 -8
- package/src/utils/raw-capture.ts +9 -2
- package/src/utils/retry.ts +17 -31
- package/src/utils/safe-io.ts +29 -7
- package/src/utils/serialization.ts +1 -4
- package/src/utils/session.ts +1 -2
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Strictness contract (differs from the old lenient passthrough on purpose):
|
|
14
14
|
* - Requests carry ONLY standard Chat Completions fields (`model`, `messages`,
|
|
15
15
|
* `tools`, `tool_choice`, `stream`, plus `max_tokens`/
|
|
16
|
-
*
|
|
16
|
+
* when explicitly set). No `reasoning`, `service_tier`,
|
|
17
17
|
* `session_id`, `prompt_cache_key`, or other extras: strict endpoints
|
|
18
18
|
* (groq, ollama, …) fail unknown body properties, so anything unsupported
|
|
19
19
|
* is warn+drop, never sent.
|
|
@@ -59,14 +59,13 @@ import { normalizeMediaInput } from "../utils/media.ts";
|
|
|
59
59
|
import { safeStringify } from "../utils/serialization.ts";
|
|
60
60
|
import { toConciseProviderError, assertModalitiesSupported } from "../utils/errors.ts";
|
|
61
61
|
import { withRetries } from "../utils/retry.ts";
|
|
62
|
-
import { ConfigError } from "../types/errors.ts";
|
|
62
|
+
import { ConfigError, ValidationError } from "../types/errors.ts";
|
|
63
63
|
import {
|
|
64
64
|
parseArguments,
|
|
65
65
|
normalizeThinkingParts,
|
|
66
66
|
newToolCallId,
|
|
67
67
|
redactedHeaders,
|
|
68
68
|
readErrorPayload,
|
|
69
|
-
incompleteStreamError,
|
|
70
69
|
hasUnfinishedToolCall,
|
|
71
70
|
linkStreamAbort,
|
|
72
71
|
withAbortTimer,
|
|
@@ -130,7 +129,10 @@ interface ChatRequestBody {
|
|
|
130
129
|
messages: ChatMessage[];
|
|
131
130
|
tools?: Array<Record<string, unknown>>;
|
|
132
131
|
tool_choice?: string | { type: "function"; function: { name: string } };
|
|
133
|
-
response_format?: {
|
|
132
|
+
response_format?: {
|
|
133
|
+
type: "json_schema";
|
|
134
|
+
json_schema: { name: string; schema: Record<string, unknown>; strict: boolean; description?: string };
|
|
135
|
+
};
|
|
134
136
|
stream?: boolean;
|
|
135
137
|
[k: string]: unknown;
|
|
136
138
|
}
|
|
@@ -231,18 +233,20 @@ function resolveApiKey(
|
|
|
231
233
|
): string | undefined {
|
|
232
234
|
const clean = (v: unknown): string | undefined => {
|
|
233
235
|
if (typeof v !== "string") return undefined;
|
|
234
|
-
const t = v
|
|
236
|
+
const t = v
|
|
237
|
+
.trim()
|
|
238
|
+
.replace(/^["']|["']$/g, "")
|
|
239
|
+
.trim();
|
|
235
240
|
return t || undefined;
|
|
236
241
|
};
|
|
237
242
|
const envPrefix = envPrefixOf(prefix);
|
|
238
|
-
const key =
|
|
243
|
+
const key =
|
|
239
244
|
clean(options?.apiKey) ||
|
|
240
245
|
clean(configuredApiKey) ||
|
|
241
246
|
clean(options?.env?.[`${envPrefix}_API_KEY`]) ||
|
|
242
247
|
clean(options?.env?.[`${envPrefix}_BASE_API_KEY`]) ||
|
|
243
248
|
getApiKey(prefix, undefined, options?.env) ||
|
|
244
|
-
undefined
|
|
245
|
-
);
|
|
249
|
+
undefined;
|
|
246
250
|
if (key) return key;
|
|
247
251
|
if (hasAuthHeader(options?.headers)) return "unused";
|
|
248
252
|
return undefined;
|
|
@@ -308,7 +312,8 @@ function warnThinkingDropped(prefix: string, thinking: ThinkingConfig | undefine
|
|
|
308
312
|
provider: prefix,
|
|
309
313
|
capability: "thinking level",
|
|
310
314
|
requested: `${level} (${modelRef})`,
|
|
311
|
-
reason:
|
|
315
|
+
reason:
|
|
316
|
+
"custom OpenAI-compatible endpoints define no portable reasoning control; levels vary by vendor and strict endpoints reject unknown fields.",
|
|
312
317
|
fallback: "the server default (no reasoning payload is sent)",
|
|
313
318
|
});
|
|
314
319
|
}
|
|
@@ -325,11 +330,7 @@ function audioFormatFor(mimeType?: string): string {
|
|
|
325
330
|
* Completions subset has no shape for them, and a provider 400 would only say
|
|
326
331
|
* so confusingly. Images and audio keep flowing natively.
|
|
327
332
|
*/
|
|
328
|
-
function assertNoVideoOrFileParts(
|
|
329
|
-
context: ProviderContext,
|
|
330
|
-
prefix: string,
|
|
331
|
-
modelId: string
|
|
332
|
-
): void {
|
|
333
|
+
function assertNoVideoOrFileParts(context: ProviderContext, prefix: string, modelId: string): void {
|
|
333
334
|
let kind: "video" | "file" | undefined;
|
|
334
335
|
for (const msg of context.messages) {
|
|
335
336
|
if (!Array.isArray((msg as { content?: unknown }).content)) continue;
|
|
@@ -350,13 +351,7 @@ function assertNoVideoOrFileParts(
|
|
|
350
351
|
kind === "video"
|
|
351
352
|
? `[${prefix}/${modelId}] unsupported video input (generic OpenAI-compatible endpoints carry text/image/audio only). Use a video-capable model (e.g. google/gemini-*) or drop the video part.`
|
|
352
353
|
: `[${prefix}/${modelId}] unsupported file input (generic OpenAI-compatible endpoints carry text/image/audio only). Use a file-capable model (e.g. google/gemini-*) or convert the document to text before sending it.`;
|
|
353
|
-
|
|
354
|
-
err.name = "AgentAccelProviderError";
|
|
355
|
-
Object.defineProperties(err, {
|
|
356
|
-
provider: { value: prefix, enumerable: false },
|
|
357
|
-
model: { value: modelId, enumerable: false },
|
|
358
|
-
});
|
|
359
|
-
throw err;
|
|
354
|
+
throw new ValidationError(message, { provider: prefix, model: modelId });
|
|
360
355
|
}
|
|
361
356
|
|
|
362
357
|
// Text, image (link or data URL) and audio parts as Chat content blocks; in: a user message's parts, out: fullHistoryMessages.
|
|
@@ -380,10 +375,7 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ChatContentPa
|
|
|
380
375
|
const raw = (part as { audio?: unknown }).audio;
|
|
381
376
|
const mimeType = (part as { mimeType?: string }).mimeType;
|
|
382
377
|
// The OpenAI chat audio shape carries data only — always inline.
|
|
383
|
-
const norm = await normalizeMediaInput(
|
|
384
|
-
raw as string | Uint8Array | ArrayBuffer,
|
|
385
|
-
mimeType
|
|
386
|
-
);
|
|
378
|
+
const norm = await normalizeMediaInput(raw as string | Uint8Array | ArrayBuffer, mimeType);
|
|
387
379
|
blocks.push({
|
|
388
380
|
type: "input_audio",
|
|
389
381
|
input_audio: { data: norm.base64Data, format: audioFormatFor(norm.mimeType) },
|
|
@@ -434,10 +426,16 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
|
|
|
434
426
|
messages.push({ role: "assistant", content: "I have processed the tool results." });
|
|
435
427
|
}
|
|
436
428
|
if (typeof m.content === "string") {
|
|
437
|
-
if (m.content) {
|
|
429
|
+
if (m.content) {
|
|
430
|
+
messages.push({ role: "user", content: m.content });
|
|
431
|
+
lastRole = "user";
|
|
432
|
+
}
|
|
438
433
|
} else {
|
|
439
434
|
const blocks = await contentPartsToBlocks(m.content);
|
|
440
|
-
if (blocks.length > 0) {
|
|
435
|
+
if (blocks.length > 0) {
|
|
436
|
+
messages.push({ role: "user", content: blocks });
|
|
437
|
+
lastRole = "user";
|
|
438
|
+
}
|
|
441
439
|
}
|
|
442
440
|
} else if (m.role === "assistant") {
|
|
443
441
|
if (typeof m.content === "string") {
|
|
@@ -533,25 +531,22 @@ function mapUsage(raw?: ChatResponse["usage"]): TokenUsage {
|
|
|
533
531
|
}
|
|
534
532
|
|
|
535
533
|
// The Gemini thought signature a proxy put on a tool call, if any; in: parseResponse, out: a string or undefined.
|
|
536
|
-
function extractSignature(rawTc: {
|
|
537
|
-
extra_content?: { google?: { thought_signature?: string } };
|
|
538
|
-
}): string | undefined {
|
|
534
|
+
function extractSignature(rawTc: { extra_content?: { google?: { thought_signature?: string } } }): string | undefined {
|
|
539
535
|
const sig = rawTc?.extra_content?.google?.thought_signature;
|
|
540
536
|
return typeof sig === "string" && sig ? sig : undefined;
|
|
541
537
|
}
|
|
542
538
|
|
|
543
539
|
/** Tags a captured compat signature with its issuing provider (`google`). */
|
|
544
|
-
function withCompatSignature(sig: string | undefined): {
|
|
540
|
+
function withCompatSignature(sig: string | undefined): {
|
|
541
|
+
thoughtSignature?: string;
|
|
542
|
+
thoughtSignatureProvider?: string;
|
|
543
|
+
} {
|
|
545
544
|
if (!sig) return {};
|
|
546
545
|
return { thoughtSignature: sig, thoughtSignatureProvider: "google" };
|
|
547
546
|
}
|
|
548
547
|
|
|
549
548
|
// Throws a concise error for an HTTP 200 reply that carries only `error`; in: parseResponse, out: a thrown error.
|
|
550
|
-
function throwResponseError(
|
|
551
|
-
response: ChatResponse,
|
|
552
|
-
prefix: string,
|
|
553
|
-
modelId: string
|
|
554
|
-
): void {
|
|
549
|
+
function throwResponseError(response: ChatResponse, prefix: string, modelId: string): void {
|
|
555
550
|
const message =
|
|
556
551
|
response.error && typeof response.error.message === "string" && response.error.message
|
|
557
552
|
? response.error.message
|
|
@@ -701,7 +696,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
701
696
|
private requireApiKey(baseUrl: string, options?: ProviderRequestOptions): string | undefined {
|
|
702
697
|
const key = this.apiKey(options);
|
|
703
698
|
if (!key && !isLocalEndpoint(baseUrl)) {
|
|
704
|
-
throw new
|
|
699
|
+
throw new ConfigError(
|
|
705
700
|
`[Agent Accelerator] Missing API key for "${this.prefix}". Set ${envPrefixOf(this.prefix)}_API_KEY or pass apiKey. (Local endpoints may omit the key.)`
|
|
706
701
|
);
|
|
707
702
|
}
|
|
@@ -722,7 +717,8 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
722
717
|
provider: this.prefix,
|
|
723
718
|
capability: "service tier",
|
|
724
719
|
requested: `${options.serviceTier} (${this.prefix}/${modelId})`,
|
|
725
|
-
reason:
|
|
720
|
+
reason:
|
|
721
|
+
"custom OpenAI-compatible endpoints define no service-tier primitive, and strict endpoints reject unknown body properties.",
|
|
726
722
|
fallback: "standard routing (no service_tier payload is sent)",
|
|
727
723
|
});
|
|
728
724
|
}
|
|
@@ -751,7 +747,8 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
751
747
|
provider: this.prefix,
|
|
752
748
|
capability: "structured output",
|
|
753
749
|
requested: `${outputSpec.name} (${this.prefix}/${modelId})`,
|
|
754
|
-
reason:
|
|
750
|
+
reason:
|
|
751
|
+
"the catalog reports structured_output: false for this model, so native response_format is unsupported.",
|
|
755
752
|
fallback: "emulated JSON instruction + client validation (no response_format is sent)",
|
|
756
753
|
});
|
|
757
754
|
}
|
|
@@ -764,7 +761,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
764
761
|
const compatTools = toCompatTools(tools);
|
|
765
762
|
if (compatTools) body.tools = compatTools;
|
|
766
763
|
// Standard Chat Completions generation fields, only when the caller set them.
|
|
767
|
-
Object.assign(body, mapGenerationParamsToChat(options
|
|
764
|
+
Object.assign(body, mapGenerationParamsToChat(options));
|
|
768
765
|
const toolChoice = mapToolChoiceToOpenAI(
|
|
769
766
|
options?.toolChoice as "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
|
|
770
767
|
);
|
|
@@ -797,7 +794,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
797
794
|
const noStreamOptions =
|
|
798
795
|
(options as { streamOptions?: unknown } | undefined)?.streamOptions === false ||
|
|
799
796
|
getEnv("COMPAT_NO_STREAM_OPTIONS") === "1" ||
|
|
800
|
-
|
|
797
|
+
options?.env?.["COMPAT_NO_STREAM_OPTIONS"] === "1";
|
|
801
798
|
if (!noStreamOptions) {
|
|
802
799
|
(body as unknown as Record<string, unknown>)["stream_options"] = { include_usage: true };
|
|
803
800
|
}
|
|
@@ -813,11 +810,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
813
810
|
sessionId: string | undefined,
|
|
814
811
|
signal?: AbortSignal
|
|
815
812
|
): RequestInit {
|
|
816
|
-
const headers: Record<string, string> = buildSessionHeaders(
|
|
817
|
-
this.prefix,
|
|
818
|
-
options?.headers,
|
|
819
|
-
sessionId
|
|
820
|
-
);
|
|
813
|
+
const headers: Record<string, string> = buildSessionHeaders(this.prefix, options?.headers, sessionId);
|
|
821
814
|
// Strict-endpoint opt-out (pi sendSessionAffinityHeaders pattern):
|
|
822
815
|
// `COMPAT_NO_SESSION_HEADERS=1` or per-call `sendSessionHeaders:false`
|
|
823
816
|
// drops affinity headers that trigger CORS preflights / strict 400s.
|
|
@@ -850,7 +843,10 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
850
843
|
sessionId: string | undefined,
|
|
851
844
|
signal?: AbortSignal
|
|
852
845
|
): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
|
|
853
|
-
const { signal: effective, cleanup } = combinedSignal(
|
|
846
|
+
const { signal: effective, cleanup } = combinedSignal(
|
|
847
|
+
signal,
|
|
848
|
+
timeoutFor(options as { timeoutMs?: number } | undefined)
|
|
849
|
+
);
|
|
854
850
|
try {
|
|
855
851
|
const res = await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, effective));
|
|
856
852
|
const text = await res.text();
|
|
@@ -945,13 +941,10 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
945
941
|
// Stateless turns never establish chains, but the session store still
|
|
946
942
|
// records the turn so other providers' switch detection keeps working.
|
|
947
943
|
noteProviderTurn(sessionId, this.prefix);
|
|
948
|
-
return parseResponse(
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
Date.now() - startTime,
|
|
953
|
-
{ request: rawRequest, response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response } }
|
|
954
|
-
);
|
|
944
|
+
return parseResponse(response, this.prefix, clean, Date.now() - startTime, {
|
|
945
|
+
request: rawRequest,
|
|
946
|
+
response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response },
|
|
947
|
+
});
|
|
955
948
|
};
|
|
956
949
|
|
|
957
950
|
try {
|
|
@@ -1006,9 +999,13 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1006
999
|
// Transient failures (429/5xx/network) are retried here, before the
|
|
1007
1000
|
// first byte; once the stream is live it is never retried.
|
|
1008
1001
|
const res = await openStreamResponse({
|
|
1009
|
-
fetchOnce: () =>
|
|
1002
|
+
fetchOnce: () =>
|
|
1003
|
+
withAbortTimer(linked, streamTimeoutMs, () =>
|
|
1004
|
+
(options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))
|
|
1005
|
+
),
|
|
1010
1006
|
onHttpError: (r, text) => this.throwIfError(r.status, url, text, clean, headersToRecord(r.headers)),
|
|
1011
|
-
noBodyError: () =>
|
|
1007
|
+
noBodyError: () =>
|
|
1008
|
+
toConciseProviderError(new Error("OpenAI-compatible streaming response had no body"), prefix, clean),
|
|
1012
1009
|
retry: retryOptionsFrom(options),
|
|
1013
1010
|
signal: linked.signal,
|
|
1014
1011
|
label: { providerId: prefix, modelId: clean },
|
|
@@ -1017,7 +1014,11 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1017
1014
|
res.headers.forEach((v, k) => {
|
|
1018
1015
|
responseHeaders[k] = v;
|
|
1019
1016
|
});
|
|
1020
|
-
const responseMeta: Record<string, unknown> & {
|
|
1017
|
+
const responseMeta: Record<string, unknown> & {
|
|
1018
|
+
status: number;
|
|
1019
|
+
statusText: string;
|
|
1020
|
+
headers: Record<string, string>;
|
|
1021
|
+
} = { status: res.status, statusText: res.statusText, headers: responseHeaders };
|
|
1021
1022
|
eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
|
|
1022
1023
|
|
|
1023
1024
|
let text = "";
|
|
@@ -1065,7 +1066,8 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1065
1066
|
if (typeof msg["id"] === "string" && !responseId) {
|
|
1066
1067
|
responseId = msg["id"] as string;
|
|
1067
1068
|
}
|
|
1068
|
-
const choice =
|
|
1069
|
+
const choice =
|
|
1070
|
+
(Array.isArray(msg["choices"]) ? (msg["choices"] as Record<string, unknown>[])[0] : undefined) ?? {};
|
|
1069
1071
|
const delta = (choice["delta"] as Record<string, unknown>) ?? {};
|
|
1070
1072
|
// Text delta (content-free accounting frames carry "" — ignored).
|
|
1071
1073
|
// Refusal deltas surface as text with a `refusal` finish.
|
package/src/providers/openai.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* effort, service_tier flex/priority, prompt_cache_key affinity, usage,
|
|
13
13
|
* finish reasons, switching, abort, errors.
|
|
14
14
|
*
|
|
15
|
-
* Also implemented: `text.format` structured output, `max_output_tokens
|
|
15
|
+
* Also implemented: `text.format` structured output, `max_output_tokens`.
|
|
16
16
|
*
|
|
17
17
|
* Out of scope (never sent): background, conversation, previous_response_id
|
|
18
18
|
* chaining, include, metadata, truncation, safety_identifier,
|
|
@@ -51,6 +51,7 @@ import { clampCacheKey } from "../utils/cache.ts";
|
|
|
51
51
|
import { normalizeMediaInput } from "../utils/media.ts";
|
|
52
52
|
import { safeStringify } from "../utils/serialization.ts";
|
|
53
53
|
import { toConciseProviderError, assertModalitiesSupported, assertNoVideoPartsOnResponses } from "../utils/errors.ts";
|
|
54
|
+
import { ConfigError } from "../types/errors.ts";
|
|
54
55
|
import { withRetries } from "../utils/retry.ts";
|
|
55
56
|
import {
|
|
56
57
|
parseArguments,
|
|
@@ -58,7 +59,6 @@ import {
|
|
|
58
59
|
newToolCallId,
|
|
59
60
|
redactedHeaders,
|
|
60
61
|
readErrorPayload,
|
|
61
|
-
incompleteStreamError,
|
|
62
62
|
hasUnfinishedToolCall,
|
|
63
63
|
linkStreamAbort,
|
|
64
64
|
withAbortTimer,
|
|
@@ -70,7 +70,6 @@ import {
|
|
|
70
70
|
combinedSignal,
|
|
71
71
|
timeoutFor,
|
|
72
72
|
hasAuthHeader,
|
|
73
|
-
clampToolCallId,
|
|
74
73
|
openStreamResponse,
|
|
75
74
|
retryOptionsFrom,
|
|
76
75
|
headersToRecord,
|
|
@@ -123,7 +122,15 @@ interface ResponsesRequestBody {
|
|
|
123
122
|
prompt_cache_key?: string;
|
|
124
123
|
store?: boolean;
|
|
125
124
|
stream?: boolean;
|
|
126
|
-
text?: {
|
|
125
|
+
text?: {
|
|
126
|
+
format: {
|
|
127
|
+
type: "json_schema";
|
|
128
|
+
name: string;
|
|
129
|
+
schema: Record<string, unknown>;
|
|
130
|
+
strict: boolean;
|
|
131
|
+
description?: string;
|
|
132
|
+
};
|
|
133
|
+
};
|
|
127
134
|
[k: string]: unknown;
|
|
128
135
|
}
|
|
129
136
|
|
|
@@ -211,13 +218,9 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ResponseConte
|
|
|
211
218
|
for (const part of parts) {
|
|
212
219
|
if (part.type === "text" && part.text) {
|
|
213
220
|
blocks.push({ type: "input_text", text: part.text });
|
|
214
|
-
} else if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
part.type === "video" ||
|
|
218
|
-
part.type === "file"
|
|
219
|
-
) {
|
|
220
|
-
const raw = (part as { image?: unknown; audio?: unknown; video?: unknown; file?: unknown }).image ??
|
|
221
|
+
} else if (part.type === "image" || part.type === "audio" || part.type === "video" || part.type === "file") {
|
|
222
|
+
const raw =
|
|
223
|
+
(part as { image?: unknown; audio?: unknown; video?: unknown; file?: unknown }).image ??
|
|
221
224
|
(part as { audio?: unknown }).audio ??
|
|
222
225
|
(part as { video?: unknown }).video ??
|
|
223
226
|
(part as { file?: unknown }).file;
|
|
@@ -318,7 +321,8 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
|
|
|
318
321
|
if (m.role === "system") continue;
|
|
319
322
|
if (m.role === "user") {
|
|
320
323
|
if (typeof m.content === "string") {
|
|
321
|
-
if (m.content)
|
|
324
|
+
if (m.content)
|
|
325
|
+
items.push({ type: "message", role: "user", content: [{ type: "input_text", text: m.content }] });
|
|
322
326
|
} else {
|
|
323
327
|
const blocks = await contentPartsToBlocks(m.content);
|
|
324
328
|
if (blocks.length > 0) items.push({ type: "message", role: "user", content: blocks });
|
|
@@ -362,7 +366,11 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
|
|
|
362
366
|
// provider-minted ids that canonical history does not retain.
|
|
363
367
|
}
|
|
364
368
|
if (texts.length > 0) {
|
|
365
|
-
items.push({
|
|
369
|
+
items.push({
|
|
370
|
+
type: "message",
|
|
371
|
+
role: "assistant",
|
|
372
|
+
content: texts.map((t) => ({ type: "output_text", text: t })),
|
|
373
|
+
});
|
|
366
374
|
}
|
|
367
375
|
} else if (m.role === "tool") {
|
|
368
376
|
if (typeof m.content === "string") {
|
|
@@ -584,8 +592,8 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
584
592
|
if (toolChoice !== undefined) body.tool_choice = toolChoice;
|
|
585
593
|
if (effort) body.reasoning = { effort };
|
|
586
594
|
if (serviceTier) body.service_tier = serviceTier;
|
|
587
|
-
// max_output_tokens
|
|
588
|
-
Object.assign(body, mapGenerationParamsToOpenAI(options
|
|
595
|
+
// max_output_tokens.
|
|
596
|
+
Object.assign(body, mapGenerationParamsToOpenAI(options));
|
|
589
597
|
// Structured outputs: Responses `text.format{type:json_schema,name,schema,strict}`.
|
|
590
598
|
// Skipped when emulated (instruction already in `instructions`).
|
|
591
599
|
if (outputSpec && !outputEmulated) {
|
|
@@ -639,11 +647,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
639
647
|
sessionId: string | undefined,
|
|
640
648
|
signal?: AbortSignal
|
|
641
649
|
): RequestInit {
|
|
642
|
-
const headers: Record<string, string> = buildSessionHeaders(
|
|
643
|
-
"openai",
|
|
644
|
-
options?.headers,
|
|
645
|
-
sessionId
|
|
646
|
-
);
|
|
650
|
+
const headers: Record<string, string> = buildSessionHeaders("openai", options?.headers, sessionId);
|
|
647
651
|
// Case-insensitive merge so a lowercase `authorization` custom header
|
|
648
652
|
// never coexists with the canonical bearer. Proxy flows
|
|
649
653
|
// (`apiKey === "unused"`) preserve custom auth instead of overwriting.
|
|
@@ -661,7 +665,10 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
661
665
|
sessionId: string | undefined,
|
|
662
666
|
signal?: AbortSignal
|
|
663
667
|
): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
|
|
664
|
-
const { signal: effective, cleanup } = combinedSignal(
|
|
668
|
+
const { signal: effective, cleanup } = combinedSignal(
|
|
669
|
+
signal,
|
|
670
|
+
timeoutFor(options as { timeoutMs?: number } | undefined)
|
|
671
|
+
);
|
|
665
672
|
try {
|
|
666
673
|
const res = await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, effective));
|
|
667
674
|
const text = await res.text();
|
|
@@ -716,7 +723,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
716
723
|
const clean = cleanModelId(model);
|
|
717
724
|
const apiKey = resolveApiKey(options);
|
|
718
725
|
if (!apiKey) {
|
|
719
|
-
throw new
|
|
726
|
+
throw new ConfigError(
|
|
720
727
|
"[Agent Accelerator] Missing OpenAI API key. Set OPENAI_API_KEY (or OPENAI_BASE_API_KEY) or pass apiKey."
|
|
721
728
|
);
|
|
722
729
|
}
|
|
@@ -768,12 +775,10 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
768
775
|
// Stateless turns never establish chains, but the session store still
|
|
769
776
|
// records the turn so other providers' switch detection keeps working.
|
|
770
777
|
noteProviderTurn(sessionId, "openai");
|
|
771
|
-
return parseResponse(
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
{ request: rawRequest, response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response } }
|
|
776
|
-
);
|
|
778
|
+
return parseResponse(response, clean, Date.now() - startTime, {
|
|
779
|
+
request: rawRequest,
|
|
780
|
+
response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response },
|
|
781
|
+
});
|
|
777
782
|
};
|
|
778
783
|
|
|
779
784
|
try {
|
|
@@ -804,7 +809,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
804
809
|
try {
|
|
805
810
|
const apiKey = resolveApiKey(options);
|
|
806
811
|
if (!apiKey) {
|
|
807
|
-
throw new
|
|
812
|
+
throw new ConfigError(
|
|
808
813
|
"[Agent Accelerator] Missing OpenAI API key. Set OPENAI_API_KEY (or OPENAI_BASE_API_KEY) or pass apiKey."
|
|
809
814
|
);
|
|
810
815
|
}
|
|
@@ -833,9 +838,13 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
833
838
|
// Transient failures (429/5xx/network) are retried here, before the
|
|
834
839
|
// first byte; once the stream is live it is never retried.
|
|
835
840
|
const res = await openStreamResponse({
|
|
836
|
-
fetchOnce: () =>
|
|
841
|
+
fetchOnce: () =>
|
|
842
|
+
withAbortTimer(linked, streamTimeoutMs, () =>
|
|
843
|
+
(options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))
|
|
844
|
+
),
|
|
837
845
|
onHttpError: (r, text) => this.throwIfError(r.status, url, text, clean, headersToRecord(r.headers)),
|
|
838
|
-
noBodyError: () =>
|
|
846
|
+
noBodyError: () =>
|
|
847
|
+
toConciseProviderError(new Error("OpenAI streaming response had no body"), "openai", clean),
|
|
839
848
|
retry: retryOptionsFrom(options),
|
|
840
849
|
signal: linked.signal,
|
|
841
850
|
label: { providerId: "openai", modelId: clean },
|
|
@@ -844,13 +853,20 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
844
853
|
res.headers.forEach((v, k) => {
|
|
845
854
|
responseHeaders[k] = v;
|
|
846
855
|
});
|
|
847
|
-
const responseMeta: Record<string, unknown> & {
|
|
856
|
+
const responseMeta: Record<string, unknown> & {
|
|
857
|
+
status: number;
|
|
858
|
+
statusText: string;
|
|
859
|
+
headers: Record<string, string>;
|
|
860
|
+
} = { status: res.status, statusText: res.statusText, headers: responseHeaders };
|
|
848
861
|
eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
|
|
849
862
|
|
|
850
863
|
let text = "";
|
|
851
864
|
let thinking = "";
|
|
852
865
|
// Tool calls keyed by output_index: { itemId, callId, name, startArgs, deltaArgs }.
|
|
853
|
-
const calls = new Map<
|
|
866
|
+
const calls = new Map<
|
|
867
|
+
number,
|
|
868
|
+
{ itemId: string; callId: string; name: string; startArgs: string; deltaArgs: string }
|
|
869
|
+
>();
|
|
854
870
|
let usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
855
871
|
let finishReason = "stop";
|
|
856
872
|
let sawFinishReason = false;
|
|
@@ -891,10 +907,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
891
907
|
} else if (type === "response.output_text.delta" && typeof msg["delta"] === "string") {
|
|
892
908
|
text += msg["delta"] as string;
|
|
893
909
|
eventStream.push({ type: "text_delta", delta: msg["delta"] as string, partialText: text });
|
|
894
|
-
} else if (
|
|
895
|
-
type === "response.refusal.delta" ||
|
|
896
|
-
type === "response.output_refusal.delta"
|
|
897
|
-
) {
|
|
910
|
+
} else if (type === "response.refusal.delta" || type === "response.output_refusal.delta") {
|
|
898
911
|
// Refusal deltas carry the filter text; surface as text and
|
|
899
912
|
// remember for a `refusal` finish reason.
|
|
900
913
|
if (typeof msg["delta"] === "string" && msg["delta"]) {
|
|
@@ -907,7 +920,11 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
907
920
|
typeof msg["delta"] === "string"
|
|
908
921
|
) {
|
|
909
922
|
thinking += msg["delta"] as string;
|
|
910
|
-
eventStream.push({
|
|
923
|
+
eventStream.push({
|
|
924
|
+
type: "thinking_delta",
|
|
925
|
+
thinkingDelta: msg["delta"] as string,
|
|
926
|
+
partialThinking: thinking,
|
|
927
|
+
});
|
|
911
928
|
} else if (type === "response.output_item.added") {
|
|
912
929
|
const index = (msg["output_index"] as number) ?? 0;
|
|
913
930
|
const item = (msg["item"] as Record<string, unknown>) ?? {};
|
|
@@ -956,7 +973,8 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
956
973
|
// Non-streaming maps `incomplete` → `length` (max tokens). Streaming
|
|
957
974
|
// must do the same — otherwise truncated turns misreport `stop`.
|
|
958
975
|
// Content-filter incompletes surface as `refusal`.
|
|
959
|
-
const incompleteReason = (response as { incomplete_details?: { reason?: string } }).incomplete_details
|
|
976
|
+
const incompleteReason = (response as { incomplete_details?: { reason?: string } }).incomplete_details
|
|
977
|
+
?.reason;
|
|
960
978
|
if (response.status === "incomplete") {
|
|
961
979
|
finishReason = incompleteReason === "content_filter" ? "refusal" : "length";
|
|
962
980
|
sawFinishReason = true;
|