agent-accelerator 0.4.0 → 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 +414 -118
- package/src/agent/context.ts +11 -16
- package/src/agent/delegation.ts +111 -274
- package/src/agent/loop.ts +327 -99
- package/src/agent/subagent.ts +4 -12
- package/src/agent/worker.ts +435 -0
- package/src/index.ts +58 -5
- package/src/models/catalog-cache.ts +65 -25
- package/src/models/catalog.ts +47 -2
- package/src/providers/google.ts +338 -81
- package/src/providers/openai-compat.ts +171 -61
- package/src/providers/openai.ts +174 -44
- package/src/providers/openrouter.ts +129 -44
- package/src/providers/shared.ts +169 -17
- package/src/providers.ts +375 -11
- 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 +229 -113
- 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 +74 -22
- package/src/types/model.ts +39 -0
- package/src/types/response.ts +9 -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/env.ts +21 -7
- package/src/utils/errors.ts +26 -1
- 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
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
|
*
|
|
@@ -59,11 +60,18 @@ import {
|
|
|
59
60
|
redactedHeaders,
|
|
60
61
|
readErrorPayload,
|
|
61
62
|
incompleteStreamError,
|
|
63
|
+
hasTruncatedToolArguments,
|
|
62
64
|
combinedSignal,
|
|
63
65
|
timeoutFor,
|
|
66
|
+
hasAuthHeader,
|
|
67
|
+
clampToolCallId,
|
|
68
|
+
openStreamResponse,
|
|
69
|
+
retryOptionsFrom,
|
|
70
|
+
headersToRecord,
|
|
71
|
+
parseRetryAfterMs,
|
|
64
72
|
} from "./shared.ts";
|
|
65
73
|
import { createGenericModelSpec } from "../models/catalog.ts";
|
|
66
|
-
import { getModelFromCatalog, getModelsForProvider } from "../models/catalog.ts";
|
|
74
|
+
import { getModelFromCatalog, getModelsForProvider, modelSupportsStructuredOutput } from "../models/catalog.ts";
|
|
67
75
|
import {
|
|
68
76
|
mapThinkingLevelToOpenAI,
|
|
69
77
|
mapServiceTierToOpenAI,
|
|
@@ -71,6 +79,12 @@ import {
|
|
|
71
79
|
mapToolChoiceToOpenAI,
|
|
72
80
|
noteProviderTurn,
|
|
73
81
|
parseStreamedToolArguments,
|
|
82
|
+
selectRawArguments,
|
|
83
|
+
mapOutputToOpenAI,
|
|
84
|
+
mapGenerationParamsToOpenAI,
|
|
85
|
+
normalizeStructuredOutput,
|
|
86
|
+
augmentSystemPromptWithStructuredOutput,
|
|
87
|
+
emitProviderWarning,
|
|
74
88
|
} from "../providers.ts";
|
|
75
89
|
|
|
76
90
|
// ---------------------------------------------------------------------------
|
|
@@ -102,6 +116,7 @@ interface ResponsesRequestBody {
|
|
|
102
116
|
prompt_cache_key?: string;
|
|
103
117
|
store?: boolean;
|
|
104
118
|
stream?: boolean;
|
|
119
|
+
text?: { format: { type: "json_schema"; name: string; schema: Record<string, unknown>; strict: boolean; description?: string } };
|
|
105
120
|
[k: string]: unknown;
|
|
106
121
|
}
|
|
107
122
|
|
|
@@ -170,7 +185,10 @@ function resolveBaseUrl(options?: ProviderRequestOptions): string {
|
|
|
170
185
|
}
|
|
171
186
|
|
|
172
187
|
function resolveApiKey(options?: ProviderRequestOptions): string | undefined {
|
|
173
|
-
|
|
188
|
+
const key = getApiKey("openai", options?.apiKey, options?.env);
|
|
189
|
+
if (key) return key;
|
|
190
|
+
if (hasAuthHeader(options?.headers)) return "unused";
|
|
191
|
+
return undefined;
|
|
174
192
|
}
|
|
175
193
|
|
|
176
194
|
/** Strips ONLY the `openai/` prefix. Bare ids pass through untouched. */
|
|
@@ -251,6 +269,20 @@ function resultToOutput(result: unknown): string {
|
|
|
251
269
|
return safeStringify(result);
|
|
252
270
|
}
|
|
253
271
|
|
|
272
|
+
function normalizeResponsesIdPart(part: string): string {
|
|
273
|
+
const sanitized = String(part ?? "").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
274
|
+
const truncated = sanitized.length > 64 ? sanitized.slice(0, 64) : sanitized;
|
|
275
|
+
return truncated.replace(/_+$/, "") || "fc_unknown";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function normalizeResponsesItemId(itemId: string): string {
|
|
279
|
+
let norm = normalizeResponsesIdPart(itemId);
|
|
280
|
+
if (!norm.startsWith("fc_")) {
|
|
281
|
+
norm = normalizeResponsesIdPart(`fc_${norm}`);
|
|
282
|
+
}
|
|
283
|
+
return norm;
|
|
284
|
+
}
|
|
285
|
+
|
|
254
286
|
/**
|
|
255
287
|
* Builds the FULL explicit history (stateless — no server state, no
|
|
256
288
|
* chaining). Assistant items intentionally carry no provider `id`/`status`:
|
|
@@ -300,10 +332,25 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
|
|
|
300
332
|
const texts: string[] = [];
|
|
301
333
|
for (const part of m.content) {
|
|
302
334
|
if (part.type === "tool_call") {
|
|
335
|
+
// Cross-provider pipe IDs (`callId|itemId`) are split and
|
|
336
|
+
// normalized (64-char, `fc_` prefix) so replay never 400s.
|
|
337
|
+
const rawId = String(part.id ?? "");
|
|
338
|
+
const rawCallId = String(part.callId ?? part.id ?? "");
|
|
339
|
+
let itemId = rawId;
|
|
340
|
+
let callId = rawCallId;
|
|
341
|
+
if (rawId.includes("|")) {
|
|
342
|
+
const [c, i] = rawId.split("|");
|
|
343
|
+
if (c) callId = c;
|
|
344
|
+
if (i) itemId = i;
|
|
345
|
+
}
|
|
346
|
+
if (callId.includes("|")) {
|
|
347
|
+
const [c] = callId.split("|");
|
|
348
|
+
if (c) callId = c;
|
|
349
|
+
}
|
|
303
350
|
items.push({
|
|
304
351
|
type: "function_call",
|
|
305
|
-
id:
|
|
306
|
-
call_id:
|
|
352
|
+
id: normalizeResponsesItemId(itemId),
|
|
353
|
+
call_id: normalizeResponsesIdPart(callId),
|
|
307
354
|
name: part.name,
|
|
308
355
|
arguments: JSON.stringify(part.arguments || {}),
|
|
309
356
|
});
|
|
@@ -319,9 +366,11 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
|
|
|
319
366
|
} else if (m.role === "tool") {
|
|
320
367
|
if (typeof m.content === "string") {
|
|
321
368
|
const queue = (m.name && idsByName.get(m.name)) || [];
|
|
369
|
+
const rawPair = queue.shift() || newToolCallId();
|
|
370
|
+
const rawCall = rawPair.includes("|") ? rawPair.split("|")[0]! : rawPair;
|
|
322
371
|
items.push({
|
|
323
372
|
type: "function_call_output",
|
|
324
|
-
call_id:
|
|
373
|
+
call_id: normalizeResponsesIdPart(rawCall),
|
|
325
374
|
output: m.content,
|
|
326
375
|
});
|
|
327
376
|
continue;
|
|
@@ -329,9 +378,11 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
|
|
|
329
378
|
if (!Array.isArray(m.content)) continue;
|
|
330
379
|
for (const part of m.content) {
|
|
331
380
|
if (part.type === "tool_result") {
|
|
381
|
+
const rawPair = pairing.get(part.id) || part.id;
|
|
382
|
+
const rawCall = rawPair.includes("|") ? rawPair.split("|")[0]! : rawPair;
|
|
332
383
|
items.push({
|
|
333
384
|
type: "function_call_output",
|
|
334
|
-
call_id:
|
|
385
|
+
call_id: normalizeResponsesIdPart(rawCall),
|
|
335
386
|
output: resultToOutput(part.result),
|
|
336
387
|
});
|
|
337
388
|
}
|
|
@@ -486,16 +537,38 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
486
537
|
const serviceTier = mapServiceTierToOpenAI(options?.serviceTier);
|
|
487
538
|
applyCacheForOpenAI(options?.cache, `openai/${modelId}`);
|
|
488
539
|
|
|
540
|
+
// Structured outputs: native `text.format` when supported, else emulated
|
|
541
|
+
// via instructions (catalog `structured_output: false` would 400).
|
|
542
|
+
let effectiveSystemPrompt = context.systemPrompt;
|
|
543
|
+
let outputEmulated = false;
|
|
544
|
+
let outputSpec: import("../types/model.ts").StructuredOutputSpec | undefined;
|
|
545
|
+
try {
|
|
546
|
+
outputSpec = normalizeStructuredOutput(options?.output ?? (options as { outputSchema?: unknown })?.outputSchema);
|
|
547
|
+
} catch {
|
|
548
|
+
outputSpec = undefined;
|
|
549
|
+
}
|
|
550
|
+
if (outputSpec && !modelSupportsStructuredOutput("openai", modelId)) {
|
|
551
|
+
outputEmulated = true;
|
|
552
|
+
effectiveSystemPrompt = augmentSystemPromptWithStructuredOutput(context.systemPrompt, outputSpec);
|
|
553
|
+
emitProviderWarning({
|
|
554
|
+
provider: "openai",
|
|
555
|
+
capability: "structured output",
|
|
556
|
+
requested: `${outputSpec.name} (openai/${modelId})`,
|
|
557
|
+
reason: "the catalog reports structured_output: false for this model, so native text.format is unsupported.",
|
|
558
|
+
fallback: "emulated JSON instruction + client validation (no text.format is sent)",
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
489
562
|
const body: ResponsesRequestBody = {
|
|
490
563
|
model: modelId,
|
|
491
|
-
input: await fullHistoryInput(context),
|
|
564
|
+
input: await fullHistoryInput({ ...context, systemPrompt: effectiveSystemPrompt }),
|
|
492
565
|
// Stateless by design: the server defaults store:true, so opt out
|
|
493
566
|
// explicitly. Full history is always sent, so no chaining is needed
|
|
494
567
|
// and provider switching stays trivial.
|
|
495
568
|
store: false,
|
|
496
569
|
...(stream ? { stream: true } : {}),
|
|
497
570
|
};
|
|
498
|
-
if (
|
|
571
|
+
if (effectiveSystemPrompt) body.instructions = effectiveSystemPrompt;
|
|
499
572
|
const aiTools = toOpenAITools(tools);
|
|
500
573
|
if (aiTools) body.tools = aiTools;
|
|
501
574
|
const toolChoice = mapToolChoiceToOpenAI(
|
|
@@ -504,8 +577,19 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
504
577
|
if (toolChoice !== undefined) body.tool_choice = toolChoice;
|
|
505
578
|
if (effort) body.reasoning = { effort };
|
|
506
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}`));
|
|
582
|
+
// Structured outputs: Responses `text.format{type:json_schema,name,schema,strict}`.
|
|
583
|
+
// Skipped when emulated (instruction already in `instructions`).
|
|
584
|
+
if (outputSpec && !outputEmulated) {
|
|
585
|
+
try {
|
|
586
|
+
const mapped = mapOutputToOpenAI(outputSpec);
|
|
587
|
+
if (mapped) body.text = { format: mapped.format };
|
|
588
|
+
} catch {
|
|
589
|
+
// why: invalid output shapes fail fast at Agent construction/loop validation.
|
|
590
|
+
}
|
|
591
|
+
}
|
|
507
592
|
// Cache affinity: prompt_cache_key replaces the legacy `user` field.
|
|
508
|
-
// (prompt_cache_options explicit breakpoints are out of scope.)
|
|
509
593
|
// OpenAI enforces max 64 chars — clamp defensively so child session ids
|
|
510
594
|
// (`parent-sub-tag-rand`) and user-supplied long ids never 400. Headers
|
|
511
595
|
// are clamped the same way via buildSessionHeaders, keeping affinity
|
|
@@ -514,6 +598,29 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
514
598
|
const cacheKey = clampCacheKey(sessionId);
|
|
515
599
|
if (cacheKey) body.prompt_cache_key = cacheKey;
|
|
516
600
|
}
|
|
601
|
+
// Long retention: `prompt_cache_retention: "24h"` when the catalog
|
|
602
|
+
// supports it (pi api/openai-responses.ts:97 rule). Other explicit modes
|
|
603
|
+
// stay warn+drop via applyCacheForOpenAI above.
|
|
604
|
+
if (options?.cache?.retention === "long") {
|
|
605
|
+
let supportsLong = true;
|
|
606
|
+
try {
|
|
607
|
+
const spec = getModelFromCatalog("openai", modelId);
|
|
608
|
+
if (spec && typeof spec.capabilities?.supportsLongCacheRetention === "boolean") {
|
|
609
|
+
supportsLong = spec.capabilities.supportsLongCacheRetention;
|
|
610
|
+
}
|
|
611
|
+
} catch {}
|
|
612
|
+
if (supportsLong) {
|
|
613
|
+
(body as Record<string, unknown>)["prompt_cache_retention"] = "24h";
|
|
614
|
+
} else {
|
|
615
|
+
emitProviderWarning({
|
|
616
|
+
provider: "openai",
|
|
617
|
+
capability: "cache retention",
|
|
618
|
+
requested: `long (openai/${modelId})`,
|
|
619
|
+
reason: "the catalog reports no long-cache support for this model.",
|
|
620
|
+
fallback: "default caching with prompt_cache_key affinity (no retention payload is sent)",
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
517
624
|
// Stateless API use: previous_response_id / background / conversation are
|
|
518
625
|
// NEVER sent (server state would break provider-agnostic switching).
|
|
519
626
|
return body;
|
|
@@ -536,9 +643,10 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
536
643
|
if (INTERNAL_HEADERS.has(k.toLowerCase())) delete headers[k];
|
|
537
644
|
}
|
|
538
645
|
// Case-insensitive merge so a lowercase `authorization` custom header
|
|
539
|
-
// never coexists with the canonical bearer.
|
|
646
|
+
// never coexists with the canonical bearer. Proxy flows
|
|
647
|
+
// (`apiKey === "unused"`) preserve custom auth instead of overwriting.
|
|
540
648
|
setHeaderCaseInsensitive(headers, "Content-Type", "application/json");
|
|
541
|
-
setHeaderCaseInsensitive(headers, "Authorization", `Bearer ${apiKey}`);
|
|
649
|
+
if (apiKey !== "unused") setHeaderCaseInsensitive(headers, "Authorization", `Bearer ${apiKey}`);
|
|
542
650
|
return { method: "POST", headers, body: JSON.stringify(body), signal };
|
|
543
651
|
}
|
|
544
652
|
|
|
@@ -552,7 +660,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
552
660
|
): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
|
|
553
661
|
const { signal: effective, cleanup } = combinedSignal(signal, timeoutFor(options as { timeoutMs?: number } | undefined));
|
|
554
662
|
try {
|
|
555
|
-
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));
|
|
556
664
|
const text = await res.text();
|
|
557
665
|
const headers: Record<string, string> = {};
|
|
558
666
|
res.headers.forEach((v, k) => {
|
|
@@ -568,17 +676,29 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
568
676
|
status: number,
|
|
569
677
|
url: string,
|
|
570
678
|
bodyText: string,
|
|
571
|
-
modelId: string
|
|
679
|
+
modelId: string,
|
|
680
|
+
headers?: Record<string, string>
|
|
572
681
|
): void {
|
|
573
682
|
if (status >= 200 && status < 300) return;
|
|
574
|
-
const { message, code, errorType } = readErrorPayload(bodyText);
|
|
575
|
-
const
|
|
683
|
+
const { message, code, errorType, raw } = readErrorPayload(bodyText);
|
|
684
|
+
const hint =
|
|
685
|
+
status === 401
|
|
686
|
+
? " Check OPENAI_API_KEY (no quotes/spaces, correct provider), OPENAI_BASE_URL, and that the key is loaded (bun --env-file=.env.local)."
|
|
687
|
+
: "";
|
|
688
|
+
const fullMessage = `${message}${hint}`;
|
|
689
|
+
const err: Record<string, unknown> & Error = new Error(fullMessage) as Record<string, unknown> & Error;
|
|
576
690
|
(err as Record<string, unknown>)["statusCode"] = status;
|
|
577
691
|
(err as Record<string, unknown>)["status"] = status;
|
|
578
692
|
(err as Record<string, unknown>)["responseBody"] = bodyText.slice(0, 500);
|
|
579
693
|
(err as Record<string, unknown>)["url"] = url.split("?")[0];
|
|
694
|
+
// Preserve hint through toConciseProviderError re-wrap (it prefers
|
|
695
|
+
// data.error.message over responseBody).
|
|
696
|
+
(err as Record<string, unknown>)["data"] = { error: { message: fullMessage } };
|
|
580
697
|
if (code !== undefined) (err as Record<string, unknown>)["code"] = code;
|
|
581
698
|
if (errorType) (err as Record<string, unknown>)["errorType"] = errorType;
|
|
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;
|
|
582
702
|
throw toConciseProviderError(err, "openai", modelId);
|
|
583
703
|
}
|
|
584
704
|
|
|
@@ -628,7 +748,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
628
748
|
|
|
629
749
|
const doCall = async (): Promise<ProviderGenerateResult> => {
|
|
630
750
|
const res = await this.doFetch(url, body, options, apiKey, sessionId, options?.signal);
|
|
631
|
-
this.throwIfError(res.status, url, res.text, clean);
|
|
751
|
+
this.throwIfError(res.status, url, res.text, clean, res.headers);
|
|
632
752
|
let response: ResponsesObject;
|
|
633
753
|
try {
|
|
634
754
|
response = JSON.parse(res.text) as ResponsesObject;
|
|
@@ -656,8 +776,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
656
776
|
|
|
657
777
|
try {
|
|
658
778
|
return await withRetries(doCall, {
|
|
659
|
-
|
|
660
|
-
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
779
|
+
...retryOptionsFrom(options),
|
|
661
780
|
signal: options?.signal,
|
|
662
781
|
label: { providerId: "openai", modelId: clean },
|
|
663
782
|
});
|
|
@@ -727,25 +846,30 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
727
846
|
|
|
728
847
|
// Q-17: optional per-call timeout races the linked abort signal.
|
|
729
848
|
const streamTimeoutMs = timeoutFor(options as { timeoutMs?: number } | undefined);
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
+
});
|
|
749
873
|
const responseHeaders: Record<string, string> = {};
|
|
750
874
|
res.headers.forEach((v, k) => {
|
|
751
875
|
responseHeaders[k] = v;
|
|
@@ -754,7 +878,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
754
878
|
eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
|
|
755
879
|
|
|
756
880
|
const parser = new SSEParser();
|
|
757
|
-
const reader = res.body
|
|
881
|
+
const reader = res.body!.getReader(); // openStreamResponse guarantees a body
|
|
758
882
|
const decoder = new TextDecoder();
|
|
759
883
|
let text = "";
|
|
760
884
|
let thinking = "";
|
|
@@ -962,7 +1086,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
962
1086
|
callId: c.callId || undefined,
|
|
963
1087
|
name: c.name,
|
|
964
1088
|
arguments: args,
|
|
965
|
-
rawArguments: c.startArgs
|
|
1089
|
+
rawArguments: selectRawArguments(c.startArgs, c.deltaArgs),
|
|
966
1090
|
};
|
|
967
1091
|
toolCalls.push(record);
|
|
968
1092
|
eventStream.push({ type: "tool_call_complete", toolCall: record });
|
|
@@ -994,6 +1118,13 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
994
1118
|
eventStream.fail(incompleteStreamError("openai", clean));
|
|
995
1119
|
return;
|
|
996
1120
|
}
|
|
1121
|
+
// Refuse unfinished tool calls whose args never parsed (output_item.done
|
|
1122
|
+
// never arrived): truncated `{raw}` must not reach the executor (pi
|
|
1123
|
+
// api/openai-responses-shared.ts:766 rule). Non-SSE mocks preserve legacy.
|
|
1124
|
+
if (!hasTerminal && toolCalls.some((c) => hasTruncatedToolArguments(c.arguments)) && isSSEPayload) {
|
|
1125
|
+
eventStream.fail(incompleteStreamError("openai", clean));
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
997
1128
|
|
|
998
1129
|
const cleanThinking = cleanThinkingEarly;
|
|
999
1130
|
const finalResponse = new AgentResponse({
|
|
@@ -1019,8 +1150,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
1019
1150
|
const isAbort =
|
|
1020
1151
|
linked.signal.aborted ||
|
|
1021
1152
|
eventStream.isCancelled() ||
|
|
1022
|
-
(raw as { name?: string }).name === "AbortError"
|
|
1023
|
-
/abort|cancell?ed/i.test(String((raw as { message?: string }).message ?? raw));
|
|
1153
|
+
(raw as { name?: string }).name === "AbortError";
|
|
1024
1154
|
eventStream.fail(
|
|
1025
1155
|
isAbort ? Object.assign(new Error("Stream aborted"), { name: "AbortError" }) : raw
|
|
1026
1156
|
);
|