agent-accelerator 0.5.9 → 0.6.5
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 +64 -16
- 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 +447 -172
- package/src/agent/architecture.md +16 -12
- package/src/agent/config-blocks.ts +39 -12
- package/src/agent/context.ts +9 -3
- package/src/agent/defaults.ts +88 -0
- package/src/agent/delegation.ts +86 -28
- package/src/agent/loop.ts +474 -374
- package/src/agent/subagent.ts +1 -3
- package/src/agent/subagents-tool.ts +50 -19
- package/src/agent/turns.ts +3 -1
- package/src/agent/usage-limit.ts +92 -0
- package/src/agent/worker.ts +73 -30
- package/src/agent/workers.ts +126 -21
- package/src/architecture.md +17 -15
- package/src/index.ts +43 -19
- package/src/models/architecture.md +4 -3
- package/src/models/catalog-cache.ts +6 -5
- package/src/models/catalog.ts +39 -30
- package/src/prompts/additional-context.md +2 -0
- package/src/prompts/architecture.md +19 -0
- package/src/prompts/prompts.ts +81 -0
- package/src/prompts/reasoning-directive.md +5 -0
- package/src/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +4 -0
- package/src/prompts/structured-output.md +2 -0
- package/src/{agent/subagents.md → prompts/subagents-tool.md} +3 -0
- package/src/prompts/tool-bridge.md +1 -0
- package/src/prompts/tool-interrupted.md +1 -0
- package/src/prompts/tool-not-found.md +1 -0
- package/src/prompts/tool-repeat-blocked.md +1 -0
- package/src/prompts/tool-schema-error.md +1 -0
- package/src/prompts/tool-truncated-arguments.md +1 -0
- package/src/prompts/worker-failed.md +1 -0
- package/src/prompts/worker-replied.md +2 -0
- package/src/providers/architecture.md +10 -8
- package/src/providers/google.ts +228 -69
- package/src/providers/openai-compat.ts +74 -56
- package/src/providers/openai.ts +64 -41
- package/src/providers/openrouter.ts +270 -48
- package/src/providers/registry.ts +6 -21
- package/src/providers/shared.ts +87 -33
- package/src/providers.ts +115 -124
- package/src/scheduler/architecture.md +8 -7
- package/src/scheduler/scheduler.ts +226 -60
- package/src/scheduler/store.ts +23 -10
- package/src/scheduler/time.ts +17 -3
- package/src/scheduler/tool.ts +38 -16
- package/src/session/architecture.md +7 -6
- package/src/session/journal.ts +39 -12
- package/src/session/store.ts +126 -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 +9 -7
- package/src/tools/executor.ts +109 -62
- package/src/tools/schema.ts +10 -6
- package/src/tools/subagent-result.ts +6 -1
- package/src/tools/tool-content.ts +32 -0
- package/src/tools/tool.ts +9 -11
- package/src/types/agent.ts +37 -22
- package/src/types/architecture.md +11 -10
- package/src/types/core.ts +5 -9
- package/src/types/errors.ts +26 -4
- package/src/types/message.ts +7 -0
- package/src/types/model.ts +4 -5
- package/src/types/scheduler.ts +4 -4
- package/src/types/tool.ts +14 -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 +9 -17
- 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 +15 -3
- 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.
|
|
@@ -60,13 +60,13 @@ import { safeStringify } from "../utils/serialization.ts";
|
|
|
60
60
|
import { toConciseProviderError, assertModalitiesSupported } from "../utils/errors.ts";
|
|
61
61
|
import { withRetries } from "../utils/retry.ts";
|
|
62
62
|
import { ConfigError, ValidationError } from "../types/errors.ts";
|
|
63
|
+
import { DEFAULT_PROMPTS } from "../prompts/prompts.ts";
|
|
63
64
|
import {
|
|
64
65
|
parseArguments,
|
|
65
66
|
normalizeThinkingParts,
|
|
66
67
|
newToolCallId,
|
|
67
68
|
redactedHeaders,
|
|
68
69
|
readErrorPayload,
|
|
69
|
-
incompleteStreamError,
|
|
70
70
|
hasUnfinishedToolCall,
|
|
71
71
|
linkStreamAbort,
|
|
72
72
|
withAbortTimer,
|
|
@@ -97,6 +97,8 @@ import {
|
|
|
97
97
|
normalizeStructuredOutput,
|
|
98
98
|
augmentSystemPromptWithStructuredOutput,
|
|
99
99
|
warnIgnoredMediaResolution,
|
|
100
|
+
warnIgnoredVideoProcessing,
|
|
101
|
+
warnIgnoredToolMedia,
|
|
100
102
|
} from "../providers.ts";
|
|
101
103
|
|
|
102
104
|
// ---------------------------------------------------------------------------
|
|
@@ -130,7 +132,10 @@ interface ChatRequestBody {
|
|
|
130
132
|
messages: ChatMessage[];
|
|
131
133
|
tools?: Array<Record<string, unknown>>;
|
|
132
134
|
tool_choice?: string | { type: "function"; function: { name: string } };
|
|
133
|
-
response_format?: {
|
|
135
|
+
response_format?: {
|
|
136
|
+
type: "json_schema";
|
|
137
|
+
json_schema: { name: string; schema: Record<string, unknown>; strict: boolean; description?: string };
|
|
138
|
+
};
|
|
134
139
|
stream?: boolean;
|
|
135
140
|
[k: string]: unknown;
|
|
136
141
|
}
|
|
@@ -231,18 +236,20 @@ function resolveApiKey(
|
|
|
231
236
|
): string | undefined {
|
|
232
237
|
const clean = (v: unknown): string | undefined => {
|
|
233
238
|
if (typeof v !== "string") return undefined;
|
|
234
|
-
const t = v
|
|
239
|
+
const t = v
|
|
240
|
+
.trim()
|
|
241
|
+
.replace(/^["']|["']$/g, "")
|
|
242
|
+
.trim();
|
|
235
243
|
return t || undefined;
|
|
236
244
|
};
|
|
237
245
|
const envPrefix = envPrefixOf(prefix);
|
|
238
|
-
const key =
|
|
246
|
+
const key =
|
|
239
247
|
clean(options?.apiKey) ||
|
|
240
248
|
clean(configuredApiKey) ||
|
|
241
249
|
clean(options?.env?.[`${envPrefix}_API_KEY`]) ||
|
|
242
250
|
clean(options?.env?.[`${envPrefix}_BASE_API_KEY`]) ||
|
|
243
251
|
getApiKey(prefix, undefined, options?.env) ||
|
|
244
|
-
undefined
|
|
245
|
-
);
|
|
252
|
+
undefined;
|
|
246
253
|
if (key) return key;
|
|
247
254
|
if (hasAuthHeader(options?.headers)) return "unused";
|
|
248
255
|
return undefined;
|
|
@@ -308,7 +315,8 @@ function warnThinkingDropped(prefix: string, thinking: ThinkingConfig | undefine
|
|
|
308
315
|
provider: prefix,
|
|
309
316
|
capability: "thinking level",
|
|
310
317
|
requested: `${level} (${modelRef})`,
|
|
311
|
-
reason:
|
|
318
|
+
reason:
|
|
319
|
+
"custom OpenAI-compatible endpoints define no portable reasoning control; levels vary by vendor and strict endpoints reject unknown fields.",
|
|
312
320
|
fallback: "the server default (no reasoning payload is sent)",
|
|
313
321
|
});
|
|
314
322
|
}
|
|
@@ -325,11 +333,7 @@ function audioFormatFor(mimeType?: string): string {
|
|
|
325
333
|
* Completions subset has no shape for them, and a provider 400 would only say
|
|
326
334
|
* so confusingly. Images and audio keep flowing natively.
|
|
327
335
|
*/
|
|
328
|
-
function assertNoVideoOrFileParts(
|
|
329
|
-
context: ProviderContext,
|
|
330
|
-
prefix: string,
|
|
331
|
-
modelId: string
|
|
332
|
-
): void {
|
|
336
|
+
function assertNoVideoOrFileParts(context: ProviderContext, prefix: string, modelId: string): void {
|
|
333
337
|
let kind: "video" | "file" | undefined;
|
|
334
338
|
for (const msg of context.messages) {
|
|
335
339
|
if (!Array.isArray((msg as { content?: unknown }).content)) continue;
|
|
@@ -374,10 +378,7 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ChatContentPa
|
|
|
374
378
|
const raw = (part as { audio?: unknown }).audio;
|
|
375
379
|
const mimeType = (part as { mimeType?: string }).mimeType;
|
|
376
380
|
// The OpenAI chat audio shape carries data only — always inline.
|
|
377
|
-
const norm = await normalizeMediaInput(
|
|
378
|
-
raw as string | Uint8Array | ArrayBuffer,
|
|
379
|
-
mimeType
|
|
380
|
-
);
|
|
381
|
+
const norm = await normalizeMediaInput(raw as string | Uint8Array | ArrayBuffer, mimeType);
|
|
381
382
|
blocks.push({
|
|
382
383
|
type: "input_audio",
|
|
383
384
|
input_audio: { data: norm.base64Data, format: audioFormatFor(norm.mimeType) },
|
|
@@ -404,7 +405,10 @@ function resultToOutput(result: unknown): string {
|
|
|
404
405
|
* `google` (or untagged legacy). Signatures minted by other providers never
|
|
405
406
|
* leak onto this transport.
|
|
406
407
|
*/
|
|
407
|
-
async function fullHistoryMessages(
|
|
408
|
+
async function fullHistoryMessages(
|
|
409
|
+
context: ProviderContext,
|
|
410
|
+
bridge = DEFAULT_PROMPTS.toolBridge
|
|
411
|
+
): Promise<ChatMessage[]> {
|
|
408
412
|
const pairing = new Map<string, string>();
|
|
409
413
|
for (const m of context.messages) {
|
|
410
414
|
if (m.role === "assistant" && Array.isArray(m.content)) {
|
|
@@ -425,13 +429,19 @@ async function fullHistoryMessages(context: ProviderContext): Promise<ChatMessag
|
|
|
425
429
|
if (m.role === "system") continue;
|
|
426
430
|
if (m.role === "user") {
|
|
427
431
|
if (lastRole === "tool") {
|
|
428
|
-
messages.push({ role: "assistant", content:
|
|
432
|
+
messages.push({ role: "assistant", content: bridge });
|
|
429
433
|
}
|
|
430
434
|
if (typeof m.content === "string") {
|
|
431
|
-
if (m.content) {
|
|
435
|
+
if (m.content) {
|
|
436
|
+
messages.push({ role: "user", content: m.content });
|
|
437
|
+
lastRole = "user";
|
|
438
|
+
}
|
|
432
439
|
} else {
|
|
433
440
|
const blocks = await contentPartsToBlocks(m.content);
|
|
434
|
-
if (blocks.length > 0) {
|
|
441
|
+
if (blocks.length > 0) {
|
|
442
|
+
messages.push({ role: "user", content: blocks });
|
|
443
|
+
lastRole = "user";
|
|
444
|
+
}
|
|
435
445
|
}
|
|
436
446
|
} else if (m.role === "assistant") {
|
|
437
447
|
if (typeof m.content === "string") {
|
|
@@ -527,25 +537,22 @@ function mapUsage(raw?: ChatResponse["usage"]): TokenUsage {
|
|
|
527
537
|
}
|
|
528
538
|
|
|
529
539
|
// The Gemini thought signature a proxy put on a tool call, if any; in: parseResponse, out: a string or undefined.
|
|
530
|
-
function extractSignature(rawTc: {
|
|
531
|
-
extra_content?: { google?: { thought_signature?: string } };
|
|
532
|
-
}): string | undefined {
|
|
540
|
+
function extractSignature(rawTc: { extra_content?: { google?: { thought_signature?: string } } }): string | undefined {
|
|
533
541
|
const sig = rawTc?.extra_content?.google?.thought_signature;
|
|
534
542
|
return typeof sig === "string" && sig ? sig : undefined;
|
|
535
543
|
}
|
|
536
544
|
|
|
537
545
|
/** Tags a captured compat signature with its issuing provider (`google`). */
|
|
538
|
-
function withCompatSignature(sig: string | undefined): {
|
|
546
|
+
function withCompatSignature(sig: string | undefined): {
|
|
547
|
+
thoughtSignature?: string;
|
|
548
|
+
thoughtSignatureProvider?: string;
|
|
549
|
+
} {
|
|
539
550
|
if (!sig) return {};
|
|
540
551
|
return { thoughtSignature: sig, thoughtSignatureProvider: "google" };
|
|
541
552
|
}
|
|
542
553
|
|
|
543
554
|
// Throws a concise error for an HTTP 200 reply that carries only `error`; in: parseResponse, out: a thrown error.
|
|
544
|
-
function throwResponseError(
|
|
545
|
-
response: ChatResponse,
|
|
546
|
-
prefix: string,
|
|
547
|
-
modelId: string
|
|
548
|
-
): void {
|
|
555
|
+
function throwResponseError(response: ChatResponse, prefix: string, modelId: string): void {
|
|
549
556
|
const message =
|
|
550
557
|
response.error && typeof response.error.message === "string" && response.error.message
|
|
551
558
|
? response.error.message
|
|
@@ -716,13 +723,16 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
716
723
|
provider: this.prefix,
|
|
717
724
|
capability: "service tier",
|
|
718
725
|
requested: `${options.serviceTier} (${this.prefix}/${modelId})`,
|
|
719
|
-
reason:
|
|
726
|
+
reason:
|
|
727
|
+
"custom OpenAI-compatible endpoints define no service-tier primitive, and strict endpoints reject unknown body properties.",
|
|
720
728
|
fallback: "standard routing (no service_tier payload is sent)",
|
|
721
729
|
});
|
|
722
730
|
}
|
|
723
731
|
// Strict endpoints reject unknown body fields: implicit caching only.
|
|
724
732
|
resolveCache(this.prefix, options?.cache, ["implicit"], `${this.prefix}/${modelId}`);
|
|
725
733
|
warnIgnoredMediaResolution(this.prefix, context.messages, `${this.prefix}/${modelId}`);
|
|
734
|
+
warnIgnoredVideoProcessing(this.prefix, context.messages, `${this.prefix}/${modelId}`);
|
|
735
|
+
warnIgnoredToolMedia(this.prefix, context.messages, `${this.prefix}/${modelId}`);
|
|
726
736
|
|
|
727
737
|
// Structured outputs: native `response_format` when supported, else
|
|
728
738
|
// emulated via system prompt (strict endpoints would 400 on json_schema
|
|
@@ -739,17 +749,22 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
739
749
|
outputEmulated = true;
|
|
740
750
|
effectiveContext = {
|
|
741
751
|
...context,
|
|
742
|
-
systemPrompt: augmentSystemPromptWithStructuredOutput(
|
|
752
|
+
systemPrompt: augmentSystemPromptWithStructuredOutput(
|
|
753
|
+
context.systemPrompt,
|
|
754
|
+
outputSpec,
|
|
755
|
+
options?.prompts?.structuredOutput
|
|
756
|
+
),
|
|
743
757
|
};
|
|
744
758
|
emitProviderWarning({
|
|
745
759
|
provider: this.prefix,
|
|
746
760
|
capability: "structured output",
|
|
747
761
|
requested: `${outputSpec.name} (${this.prefix}/${modelId})`,
|
|
748
|
-
reason:
|
|
762
|
+
reason:
|
|
763
|
+
"the catalog reports structured_output: false for this model, so native response_format is unsupported.",
|
|
749
764
|
fallback: "emulated JSON instruction + client validation (no response_format is sent)",
|
|
750
765
|
});
|
|
751
766
|
}
|
|
752
|
-
const messages = await fullHistoryMessages(effectiveContext);
|
|
767
|
+
const messages = await fullHistoryMessages(effectiveContext, options?.prompts?.toolBridge);
|
|
753
768
|
const body: ChatRequestBody = {
|
|
754
769
|
model: modelId,
|
|
755
770
|
messages,
|
|
@@ -758,10 +773,8 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
758
773
|
const compatTools = toCompatTools(tools);
|
|
759
774
|
if (compatTools) body.tools = compatTools;
|
|
760
775
|
// Standard Chat Completions generation fields, only when the caller set them.
|
|
761
|
-
Object.assign(body, mapGenerationParamsToChat(options
|
|
762
|
-
const toolChoice = mapToolChoiceToOpenAI(
|
|
763
|
-
options?.toolChoice as "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
|
|
764
|
-
);
|
|
776
|
+
Object.assign(body, mapGenerationParamsToChat(options));
|
|
777
|
+
const toolChoice = mapToolChoiceToOpenAI(options?.toolChoice, this.prefix);
|
|
765
778
|
if (toolChoice !== undefined) {
|
|
766
779
|
// Chat Completions nests pinned names under `function`; the canonical
|
|
767
780
|
// mapper emits the flat Responses shape, so normalize here.
|
|
@@ -791,7 +804,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
791
804
|
const noStreamOptions =
|
|
792
805
|
(options as { streamOptions?: unknown } | undefined)?.streamOptions === false ||
|
|
793
806
|
getEnv("COMPAT_NO_STREAM_OPTIONS") === "1" ||
|
|
794
|
-
|
|
807
|
+
options?.env?.["COMPAT_NO_STREAM_OPTIONS"] === "1";
|
|
795
808
|
if (!noStreamOptions) {
|
|
796
809
|
(body as unknown as Record<string, unknown>)["stream_options"] = { include_usage: true };
|
|
797
810
|
}
|
|
@@ -807,11 +820,7 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
807
820
|
sessionId: string | undefined,
|
|
808
821
|
signal?: AbortSignal
|
|
809
822
|
): RequestInit {
|
|
810
|
-
const headers: Record<string, string> = buildSessionHeaders(
|
|
811
|
-
this.prefix,
|
|
812
|
-
options?.headers,
|
|
813
|
-
sessionId
|
|
814
|
-
);
|
|
823
|
+
const headers: Record<string, string> = buildSessionHeaders(this.prefix, options?.headers, sessionId);
|
|
815
824
|
// Strict-endpoint opt-out (pi sendSessionAffinityHeaders pattern):
|
|
816
825
|
// `COMPAT_NO_SESSION_HEADERS=1` or per-call `sendSessionHeaders:false`
|
|
817
826
|
// drops affinity headers that trigger CORS preflights / strict 400s.
|
|
@@ -844,7 +853,10 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
844
853
|
sessionId: string | undefined,
|
|
845
854
|
signal?: AbortSignal
|
|
846
855
|
): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
|
|
847
|
-
const { signal: effective, cleanup } = combinedSignal(
|
|
856
|
+
const { signal: effective, cleanup } = combinedSignal(
|
|
857
|
+
signal,
|
|
858
|
+
timeoutFor(options as { timeoutMs?: number } | undefined)
|
|
859
|
+
);
|
|
848
860
|
try {
|
|
849
861
|
const res = await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, effective));
|
|
850
862
|
const text = await res.text();
|
|
@@ -939,13 +951,10 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
939
951
|
// Stateless turns never establish chains, but the session store still
|
|
940
952
|
// records the turn so other providers' switch detection keeps working.
|
|
941
953
|
noteProviderTurn(sessionId, this.prefix);
|
|
942
|
-
return parseResponse(
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
Date.now() - startTime,
|
|
947
|
-
{ request: rawRequest, response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response } }
|
|
948
|
-
);
|
|
954
|
+
return parseResponse(response, this.prefix, clean, Date.now() - startTime, {
|
|
955
|
+
request: rawRequest,
|
|
956
|
+
response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response },
|
|
957
|
+
});
|
|
949
958
|
};
|
|
950
959
|
|
|
951
960
|
try {
|
|
@@ -1000,9 +1009,13 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1000
1009
|
// Transient failures (429/5xx/network) are retried here, before the
|
|
1001
1010
|
// first byte; once the stream is live it is never retried.
|
|
1002
1011
|
const res = await openStreamResponse({
|
|
1003
|
-
fetchOnce: () =>
|
|
1012
|
+
fetchOnce: () =>
|
|
1013
|
+
withAbortTimer(linked, streamTimeoutMs, () =>
|
|
1014
|
+
(options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))
|
|
1015
|
+
),
|
|
1004
1016
|
onHttpError: (r, text) => this.throwIfError(r.status, url, text, clean, headersToRecord(r.headers)),
|
|
1005
|
-
noBodyError: () =>
|
|
1017
|
+
noBodyError: () =>
|
|
1018
|
+
toConciseProviderError(new Error("OpenAI-compatible streaming response had no body"), prefix, clean),
|
|
1006
1019
|
retry: retryOptionsFrom(options),
|
|
1007
1020
|
signal: linked.signal,
|
|
1008
1021
|
label: { providerId: prefix, modelId: clean },
|
|
@@ -1011,7 +1024,11 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1011
1024
|
res.headers.forEach((v, k) => {
|
|
1012
1025
|
responseHeaders[k] = v;
|
|
1013
1026
|
});
|
|
1014
|
-
const responseMeta: Record<string, unknown> & {
|
|
1027
|
+
const responseMeta: Record<string, unknown> & {
|
|
1028
|
+
status: number;
|
|
1029
|
+
statusText: string;
|
|
1030
|
+
headers: Record<string, string>;
|
|
1031
|
+
} = { status: res.status, statusText: res.statusText, headers: responseHeaders };
|
|
1015
1032
|
eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
|
|
1016
1033
|
|
|
1017
1034
|
let text = "";
|
|
@@ -1059,7 +1076,8 @@ export class OpenAICompatibleChatProvider implements Provider {
|
|
|
1059
1076
|
if (typeof msg["id"] === "string" && !responseId) {
|
|
1060
1077
|
responseId = msg["id"] as string;
|
|
1061
1078
|
}
|
|
1062
|
-
const choice =
|
|
1079
|
+
const choice =
|
|
1080
|
+
(Array.isArray(msg["choices"]) ? (msg["choices"] as Record<string, unknown>[])[0] : undefined) ?? {};
|
|
1063
1081
|
const delta = (choice["delta"] as Record<string, unknown>) ?? {};
|
|
1064
1082
|
// Text delta (content-free accounting frames carry "" — ignored).
|
|
1065
1083
|
// 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,
|
|
@@ -59,7 +59,6 @@ import {
|
|
|
59
59
|
newToolCallId,
|
|
60
60
|
redactedHeaders,
|
|
61
61
|
readErrorPayload,
|
|
62
|
-
incompleteStreamError,
|
|
63
62
|
hasUnfinishedToolCall,
|
|
64
63
|
linkStreamAbort,
|
|
65
64
|
withAbortTimer,
|
|
@@ -71,7 +70,6 @@ import {
|
|
|
71
70
|
combinedSignal,
|
|
72
71
|
timeoutFor,
|
|
73
72
|
hasAuthHeader,
|
|
74
|
-
clampToolCallId,
|
|
75
73
|
openStreamResponse,
|
|
76
74
|
retryOptionsFrom,
|
|
77
75
|
headersToRecord,
|
|
@@ -93,6 +91,8 @@ import {
|
|
|
93
91
|
augmentSystemPromptWithStructuredOutput,
|
|
94
92
|
emitProviderWarning,
|
|
95
93
|
warnIgnoredMediaResolution,
|
|
94
|
+
warnIgnoredVideoProcessing,
|
|
95
|
+
warnIgnoredToolMedia,
|
|
96
96
|
} from "../providers.ts";
|
|
97
97
|
|
|
98
98
|
// ---------------------------------------------------------------------------
|
|
@@ -124,7 +124,15 @@ interface ResponsesRequestBody {
|
|
|
124
124
|
prompt_cache_key?: string;
|
|
125
125
|
store?: boolean;
|
|
126
126
|
stream?: boolean;
|
|
127
|
-
text?: {
|
|
127
|
+
text?: {
|
|
128
|
+
format: {
|
|
129
|
+
type: "json_schema";
|
|
130
|
+
name: string;
|
|
131
|
+
schema: Record<string, unknown>;
|
|
132
|
+
strict: boolean;
|
|
133
|
+
description?: string;
|
|
134
|
+
};
|
|
135
|
+
};
|
|
128
136
|
[k: string]: unknown;
|
|
129
137
|
}
|
|
130
138
|
|
|
@@ -212,13 +220,9 @@ async function contentPartsToBlocks(parts: ContentPart[]): Promise<ResponseConte
|
|
|
212
220
|
for (const part of parts) {
|
|
213
221
|
if (part.type === "text" && part.text) {
|
|
214
222
|
blocks.push({ type: "input_text", text: part.text });
|
|
215
|
-
} else if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
part.type === "video" ||
|
|
219
|
-
part.type === "file"
|
|
220
|
-
) {
|
|
221
|
-
const raw = (part as { image?: unknown; audio?: unknown; video?: unknown; file?: unknown }).image ??
|
|
223
|
+
} else if (part.type === "image" || part.type === "audio" || part.type === "video" || part.type === "file") {
|
|
224
|
+
const raw =
|
|
225
|
+
(part as { image?: unknown; audio?: unknown; video?: unknown; file?: unknown }).image ??
|
|
222
226
|
(part as { audio?: unknown }).audio ??
|
|
223
227
|
(part as { video?: unknown }).video ??
|
|
224
228
|
(part as { file?: unknown }).file;
|
|
@@ -319,7 +323,8 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
|
|
|
319
323
|
if (m.role === "system") continue;
|
|
320
324
|
if (m.role === "user") {
|
|
321
325
|
if (typeof m.content === "string") {
|
|
322
|
-
if (m.content)
|
|
326
|
+
if (m.content)
|
|
327
|
+
items.push({ type: "message", role: "user", content: [{ type: "input_text", text: m.content }] });
|
|
323
328
|
} else {
|
|
324
329
|
const blocks = await contentPartsToBlocks(m.content);
|
|
325
330
|
if (blocks.length > 0) items.push({ type: "message", role: "user", content: blocks });
|
|
@@ -363,7 +368,11 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
|
|
|
363
368
|
// provider-minted ids that canonical history does not retain.
|
|
364
369
|
}
|
|
365
370
|
if (texts.length > 0) {
|
|
366
|
-
items.push({
|
|
371
|
+
items.push({
|
|
372
|
+
type: "message",
|
|
373
|
+
role: "assistant",
|
|
374
|
+
content: texts.map((t) => ({ type: "output_text", text: t })),
|
|
375
|
+
});
|
|
367
376
|
}
|
|
368
377
|
} else if (m.role === "tool") {
|
|
369
378
|
if (typeof m.content === "string") {
|
|
@@ -544,6 +553,8 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
544
553
|
// `12h` maps to `prompt_cache_retention: "24h"`; everything else stays on implicit prompt_cache_key affinity.
|
|
545
554
|
const cache = resolveCache("openai", options?.cache, ["implicit", "12h"], `openai/${modelId}`);
|
|
546
555
|
warnIgnoredMediaResolution("openai", context.messages, `openai/${modelId}`);
|
|
556
|
+
warnIgnoredVideoProcessing("openai", context.messages, `openai/${modelId}`);
|
|
557
|
+
warnIgnoredToolMedia("openai", context.messages, `openai/${modelId}`);
|
|
547
558
|
|
|
548
559
|
// Structured outputs: native `text.format` when supported, else emulated
|
|
549
560
|
// via instructions (catalog `structured_output: false` would 400).
|
|
@@ -557,7 +568,11 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
557
568
|
}
|
|
558
569
|
if (outputSpec && !modelSupportsStructuredOutput("openai", modelId)) {
|
|
559
570
|
outputEmulated = true;
|
|
560
|
-
effectiveSystemPrompt = augmentSystemPromptWithStructuredOutput(
|
|
571
|
+
effectiveSystemPrompt = augmentSystemPromptWithStructuredOutput(
|
|
572
|
+
context.systemPrompt,
|
|
573
|
+
outputSpec,
|
|
574
|
+
options?.prompts?.structuredOutput
|
|
575
|
+
);
|
|
561
576
|
emitProviderWarning({
|
|
562
577
|
provider: "openai",
|
|
563
578
|
capability: "structured output",
|
|
@@ -579,14 +594,12 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
579
594
|
if (effectiveSystemPrompt) body.instructions = effectiveSystemPrompt;
|
|
580
595
|
const aiTools = toOpenAITools(tools);
|
|
581
596
|
if (aiTools) body.tools = aiTools;
|
|
582
|
-
const toolChoice = mapToolChoiceToOpenAI(
|
|
583
|
-
options?.toolChoice as "auto" | "none" | "required" | { type: "function"; function: { name: string } } | undefined
|
|
584
|
-
);
|
|
597
|
+
const toolChoice = mapToolChoiceToOpenAI(options?.toolChoice);
|
|
585
598
|
if (toolChoice !== undefined) body.tool_choice = toolChoice;
|
|
586
599
|
if (effort) body.reasoning = { effort };
|
|
587
600
|
if (serviceTier) body.service_tier = serviceTier;
|
|
588
|
-
// max_output_tokens
|
|
589
|
-
Object.assign(body, mapGenerationParamsToOpenAI(options
|
|
601
|
+
// max_output_tokens.
|
|
602
|
+
Object.assign(body, mapGenerationParamsToOpenAI(options));
|
|
590
603
|
// Structured outputs: Responses `text.format{type:json_schema,name,schema,strict}`.
|
|
591
604
|
// Skipped when emulated (instruction already in `instructions`).
|
|
592
605
|
if (outputSpec && !outputEmulated) {
|
|
@@ -640,11 +653,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
640
653
|
sessionId: string | undefined,
|
|
641
654
|
signal?: AbortSignal
|
|
642
655
|
): RequestInit {
|
|
643
|
-
const headers: Record<string, string> = buildSessionHeaders(
|
|
644
|
-
"openai",
|
|
645
|
-
options?.headers,
|
|
646
|
-
sessionId
|
|
647
|
-
);
|
|
656
|
+
const headers: Record<string, string> = buildSessionHeaders("openai", options?.headers, sessionId);
|
|
648
657
|
// Case-insensitive merge so a lowercase `authorization` custom header
|
|
649
658
|
// never coexists with the canonical bearer. Proxy flows
|
|
650
659
|
// (`apiKey === "unused"`) preserve custom auth instead of overwriting.
|
|
@@ -662,7 +671,10 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
662
671
|
sessionId: string | undefined,
|
|
663
672
|
signal?: AbortSignal
|
|
664
673
|
): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
|
|
665
|
-
const { signal: effective, cleanup } = combinedSignal(
|
|
674
|
+
const { signal: effective, cleanup } = combinedSignal(
|
|
675
|
+
signal,
|
|
676
|
+
timeoutFor(options as { timeoutMs?: number } | undefined)
|
|
677
|
+
);
|
|
666
678
|
try {
|
|
667
679
|
const res = await (options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, effective));
|
|
668
680
|
const text = await res.text();
|
|
@@ -769,12 +781,10 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
769
781
|
// Stateless turns never establish chains, but the session store still
|
|
770
782
|
// records the turn so other providers' switch detection keeps working.
|
|
771
783
|
noteProviderTurn(sessionId, "openai");
|
|
772
|
-
return parseResponse(
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
{ request: rawRequest, response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response } }
|
|
777
|
-
);
|
|
784
|
+
return parseResponse(response, clean, Date.now() - startTime, {
|
|
785
|
+
request: rawRequest,
|
|
786
|
+
response: { status: res.status, statusText: res.statusText, headers: res.headers, body: response },
|
|
787
|
+
});
|
|
778
788
|
};
|
|
779
789
|
|
|
780
790
|
try {
|
|
@@ -834,9 +844,13 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
834
844
|
// Transient failures (429/5xx/network) are retried here, before the
|
|
835
845
|
// first byte; once the stream is live it is never retried.
|
|
836
846
|
const res = await openStreamResponse({
|
|
837
|
-
fetchOnce: () =>
|
|
847
|
+
fetchOnce: () =>
|
|
848
|
+
withAbortTimer(linked, streamTimeoutMs, () =>
|
|
849
|
+
(options?.fetch ?? fetch)(url, this.requestInit(body, options, apiKey, sessionId, linked.signal))
|
|
850
|
+
),
|
|
838
851
|
onHttpError: (r, text) => this.throwIfError(r.status, url, text, clean, headersToRecord(r.headers)),
|
|
839
|
-
noBodyError: () =>
|
|
852
|
+
noBodyError: () =>
|
|
853
|
+
toConciseProviderError(new Error("OpenAI streaming response had no body"), "openai", clean),
|
|
840
854
|
retry: retryOptionsFrom(options),
|
|
841
855
|
signal: linked.signal,
|
|
842
856
|
label: { providerId: "openai", modelId: clean },
|
|
@@ -845,13 +859,20 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
845
859
|
res.headers.forEach((v, k) => {
|
|
846
860
|
responseHeaders[k] = v;
|
|
847
861
|
});
|
|
848
|
-
const responseMeta: Record<string, unknown> & {
|
|
862
|
+
const responseMeta: Record<string, unknown> & {
|
|
863
|
+
status: number;
|
|
864
|
+
statusText: string;
|
|
865
|
+
headers: Record<string, string>;
|
|
866
|
+
} = { status: res.status, statusText: res.statusText, headers: responseHeaders };
|
|
849
867
|
eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
|
|
850
868
|
|
|
851
869
|
let text = "";
|
|
852
870
|
let thinking = "";
|
|
853
871
|
// Tool calls keyed by output_index: { itemId, callId, name, startArgs, deltaArgs }.
|
|
854
|
-
const calls = new Map<
|
|
872
|
+
const calls = new Map<
|
|
873
|
+
number,
|
|
874
|
+
{ itemId: string; callId: string; name: string; startArgs: string; deltaArgs: string }
|
|
875
|
+
>();
|
|
855
876
|
let usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
856
877
|
let finishReason = "stop";
|
|
857
878
|
let sawFinishReason = false;
|
|
@@ -892,10 +913,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
892
913
|
} else if (type === "response.output_text.delta" && typeof msg["delta"] === "string") {
|
|
893
914
|
text += msg["delta"] as string;
|
|
894
915
|
eventStream.push({ type: "text_delta", delta: msg["delta"] as string, partialText: text });
|
|
895
|
-
} else if (
|
|
896
|
-
type === "response.refusal.delta" ||
|
|
897
|
-
type === "response.output_refusal.delta"
|
|
898
|
-
) {
|
|
916
|
+
} else if (type === "response.refusal.delta" || type === "response.output_refusal.delta") {
|
|
899
917
|
// Refusal deltas carry the filter text; surface as text and
|
|
900
918
|
// remember for a `refusal` finish reason.
|
|
901
919
|
if (typeof msg["delta"] === "string" && msg["delta"]) {
|
|
@@ -908,7 +926,11 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
908
926
|
typeof msg["delta"] === "string"
|
|
909
927
|
) {
|
|
910
928
|
thinking += msg["delta"] as string;
|
|
911
|
-
eventStream.push({
|
|
929
|
+
eventStream.push({
|
|
930
|
+
type: "thinking_delta",
|
|
931
|
+
thinkingDelta: msg["delta"] as string,
|
|
932
|
+
partialThinking: thinking,
|
|
933
|
+
});
|
|
912
934
|
} else if (type === "response.output_item.added") {
|
|
913
935
|
const index = (msg["output_index"] as number) ?? 0;
|
|
914
936
|
const item = (msg["item"] as Record<string, unknown>) ?? {};
|
|
@@ -957,7 +979,8 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
957
979
|
// Non-streaming maps `incomplete` → `length` (max tokens). Streaming
|
|
958
980
|
// must do the same — otherwise truncated turns misreport `stop`.
|
|
959
981
|
// Content-filter incompletes surface as `refusal`.
|
|
960
|
-
const incompleteReason = (response as { incomplete_details?: { reason?: string } }).incomplete_details
|
|
982
|
+
const incompleteReason = (response as { incomplete_details?: { reason?: string } }).incomplete_details
|
|
983
|
+
?.reason;
|
|
961
984
|
if (response.status === "incomplete") {
|
|
962
985
|
finishReason = incompleteReason === "content_filter" ? "refusal" : "length";
|
|
963
986
|
sawFinishReason = true;
|