agent-accelerator 0.3.0 → 0.4.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/package.json +1 -1
- package/src/agent/agent.ts +830 -116
- package/src/agent/context.ts +35 -0
- package/src/agent/delegation.ts +390 -36
- package/src/agent/loop.ts +711 -109
- package/src/agent/subagent.ts +59 -0
- package/src/index.ts +154 -59
- package/src/models/catalog-cache.ts +68 -25
- package/src/models/catalog.ts +97 -18
- package/src/providers/google.ts +99 -39
- package/src/providers/openai-compat.ts +140 -80
- package/src/providers/openai.ts +149 -69
- package/src/providers/openrouter.ts +106 -71
- package/src/providers/registry.ts +16 -12
- package/src/providers/shared.ts +132 -0
- package/src/providers.ts +3 -4
- package/src/session/store.ts +512 -33
- package/src/streaming/event-stream.ts +156 -41
- package/src/tools/executor.ts +248 -15
- package/src/tools/tool.ts +147 -3
- package/src/types/agent.ts +229 -2
- package/src/types/errors.ts +133 -0
- package/src/types/model.ts +1 -1
- package/src/types/response.ts +126 -3
- package/src/types/tool.ts +69 -0
- package/src/update-models.ts +8 -2
- package/src/utils/cache.ts +68 -1
- package/src/utils/env.ts +2 -6
- package/src/utils/headers.ts +153 -19
- package/src/utils/retry.ts +27 -13
- package/src/utils/session.ts +284 -0
- package/src/providers/openrouter-responses.ts +0 -949
- package/src/types/provider-payloads.ts +0 -352
package/src/providers/openai.ts
CHANGED
|
@@ -46,12 +46,22 @@ import { AssistantMessageEventStream } from "../streaming/event-stream.ts";
|
|
|
46
46
|
import { SSEParser } from "../streaming/sse-parser.ts";
|
|
47
47
|
import { AgentResponse } from "../types/response.ts";
|
|
48
48
|
import { getApiKey, getEnv } from "../utils/env.ts";
|
|
49
|
-
import { buildSessionHeaders } from "../utils/headers.ts";
|
|
49
|
+
import { buildSessionHeaders, setHeaderCaseInsensitive } from "../utils/headers.ts";
|
|
50
50
|
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
54
|
import { withRetries } from "../utils/retry.ts";
|
|
55
|
+
import {
|
|
56
|
+
parseArguments,
|
|
57
|
+
normalizeThinkingParts,
|
|
58
|
+
newToolCallId,
|
|
59
|
+
redactedHeaders,
|
|
60
|
+
readErrorPayload,
|
|
61
|
+
incompleteStreamError,
|
|
62
|
+
combinedSignal,
|
|
63
|
+
timeoutFor,
|
|
64
|
+
} from "./shared.ts";
|
|
55
65
|
import { createGenericModelSpec } from "../models/catalog.ts";
|
|
56
66
|
import { getModelFromCatalog, getModelsForProvider } from "../models/catalog.ts";
|
|
57
67
|
import {
|
|
@@ -131,6 +141,12 @@ interface ResponsesObject {
|
|
|
131
141
|
|
|
132
142
|
const DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
133
143
|
|
|
144
|
+
/** Q-18: Responses transport never echoes thought signatures (no wire field).
|
|
145
|
+
* Local helper documents the namespacing decision per provider file. */
|
|
146
|
+
function withProviderSignature(_sig: string | undefined): { thoughtSignature?: undefined } {
|
|
147
|
+
return {};
|
|
148
|
+
}
|
|
149
|
+
|
|
134
150
|
/** Internal headers that must never leak onto native REST requests. */
|
|
135
151
|
const INTERNAL_HEADERS = new Set([
|
|
136
152
|
"x-thought-signature-map",
|
|
@@ -305,7 +321,7 @@ async function fullHistoryInput(context: ProviderContext): Promise<ResponseInput
|
|
|
305
321
|
const queue = (m.name && idsByName.get(m.name)) || [];
|
|
306
322
|
items.push({
|
|
307
323
|
type: "function_call_output",
|
|
308
|
-
call_id: queue.shift() ||
|
|
324
|
+
call_id: queue.shift() || newToolCallId(),
|
|
309
325
|
output: m.content,
|
|
310
326
|
});
|
|
311
327
|
continue;
|
|
@@ -345,27 +361,6 @@ function mapUsage(raw?: ResponsesObject["usage"]): TokenUsage {
|
|
|
345
361
|
};
|
|
346
362
|
}
|
|
347
363
|
|
|
348
|
-
function parseArguments(raw: string | undefined): Record<string, unknown> {
|
|
349
|
-
if (!raw) return {};
|
|
350
|
-
try {
|
|
351
|
-
const parsed: unknown = JSON.parse(raw);
|
|
352
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
353
|
-
return parsed as Record<string, unknown>;
|
|
354
|
-
}
|
|
355
|
-
return { raw };
|
|
356
|
-
} catch {
|
|
357
|
-
return { raw };
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/** Trims/collapses assembled thinking parts (no edge-tripling). */
|
|
362
|
-
function normalizeThinkingParts(parts: string[]): string | undefined {
|
|
363
|
-
const cleaned = parts
|
|
364
|
-
.map((p) => p.replace(/\n{3,}/g, "\n\n").trim())
|
|
365
|
-
.filter((p) => p.length > 0);
|
|
366
|
-
return cleaned.length > 0 ? cleaned.join("\n") : undefined;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
364
|
function parseResponse(
|
|
370
365
|
response: ResponsesObject,
|
|
371
366
|
modelId: string,
|
|
@@ -384,6 +379,7 @@ function parseResponse(
|
|
|
384
379
|
}
|
|
385
380
|
|
|
386
381
|
let text = "";
|
|
382
|
+
let refusalText = "";
|
|
387
383
|
const thinkingParts: string[] = [];
|
|
388
384
|
const toolCalls: ToolCallRecord[] = [];
|
|
389
385
|
|
|
@@ -391,6 +387,15 @@ function parseResponse(
|
|
|
391
387
|
if (item.type === "message") {
|
|
392
388
|
for (const block of item.content ?? []) {
|
|
393
389
|
if (block.type === "output_text" && block.text) text += block.text;
|
|
390
|
+
else if (block.type === "refusal" && block.text) {
|
|
391
|
+
text += block.text;
|
|
392
|
+
refusalText += block.text;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const itemRefusal = (item as { refusal?: unknown }).refusal;
|
|
396
|
+
if (typeof itemRefusal === "string" && itemRefusal) {
|
|
397
|
+
text += itemRefusal;
|
|
398
|
+
refusalText += itemRefusal;
|
|
394
399
|
}
|
|
395
400
|
} else if (item.type === "reasoning") {
|
|
396
401
|
for (const block of item.content ?? []) {
|
|
@@ -404,7 +409,7 @@ function parseResponse(
|
|
|
404
409
|
} else if (item.type === "function_call") {
|
|
405
410
|
const args = parseArguments(item.arguments);
|
|
406
411
|
toolCalls.push({
|
|
407
|
-
id: item.id || item.call_id ||
|
|
412
|
+
id: item.id || item.call_id || newToolCallId(),
|
|
408
413
|
callId: item.call_id,
|
|
409
414
|
name: item.name || "unknown",
|
|
410
415
|
arguments: args,
|
|
@@ -413,6 +418,17 @@ function parseResponse(
|
|
|
413
418
|
}
|
|
414
419
|
}
|
|
415
420
|
|
|
421
|
+
// Q-23: `incomplete` -> length (content-filter truncations surface as
|
|
422
|
+
// refusal when refusal text is present).
|
|
423
|
+
const incompleteReason = (response as { incomplete_details?: { reason?: string } }).incomplete_details?.reason;
|
|
424
|
+
const finishReason =
|
|
425
|
+
toolCalls.length > 0
|
|
426
|
+
? "tool_calls"
|
|
427
|
+
: refusalText || incompleteReason === "content_filter"
|
|
428
|
+
? "refusal"
|
|
429
|
+
: response.status === "incomplete"
|
|
430
|
+
? "length"
|
|
431
|
+
: "stop";
|
|
416
432
|
return {
|
|
417
433
|
text,
|
|
418
434
|
// Trim/collapse reasoning assembly (provider summaries can trail with
|
|
@@ -422,7 +438,7 @@ function parseResponse(
|
|
|
422
438
|
thoughtSignature: undefined,
|
|
423
439
|
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
424
440
|
usage: mapUsage(response.usage),
|
|
425
|
-
finishReason
|
|
441
|
+
finishReason,
|
|
426
442
|
responseId: response.id,
|
|
427
443
|
model: modelId,
|
|
428
444
|
provider: "openai",
|
|
@@ -431,32 +447,6 @@ function parseResponse(
|
|
|
431
447
|
};
|
|
432
448
|
}
|
|
433
449
|
|
|
434
|
-
function redactedHeaders(headers: Record<string, string>): Record<string, string> {
|
|
435
|
-
const out: Record<string, string> = {};
|
|
436
|
-
for (const [k, v] of Object.entries(headers)) {
|
|
437
|
-
out[k] = k.toLowerCase() === "authorization" ? "[REDACTED]" : v;
|
|
438
|
-
}
|
|
439
|
-
return out;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function readErrorPayload(bodyText: string): { message: string; code?: string | number; errorType?: string } {
|
|
443
|
-
try {
|
|
444
|
-
const parsed: unknown = JSON.parse(bodyText);
|
|
445
|
-
const first = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
446
|
-
const err = (first as { error?: { message?: string; code?: string | number; type?: string } })?.error;
|
|
447
|
-
if (err && typeof err.message === "string") {
|
|
448
|
-
return {
|
|
449
|
-
message: err.message,
|
|
450
|
-
code: err.code,
|
|
451
|
-
...(typeof err.type === "string" ? { errorType: err.type } : {}),
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
return { message: bodyText.slice(0, 300) };
|
|
455
|
-
} catch {
|
|
456
|
-
return { message: bodyText.slice(0, 300) };
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
450
|
// ---------------------------------------------------------------------------
|
|
461
451
|
// Provider
|
|
462
452
|
// ---------------------------------------------------------------------------
|
|
@@ -545,8 +535,10 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
545
535
|
for (const [k, v] of Object.entries(headers)) {
|
|
546
536
|
if (INTERNAL_HEADERS.has(k.toLowerCase())) delete headers[k];
|
|
547
537
|
}
|
|
548
|
-
|
|
549
|
-
|
|
538
|
+
// Case-insensitive merge so a lowercase `authorization` custom header
|
|
539
|
+
// never coexists with the canonical bearer.
|
|
540
|
+
setHeaderCaseInsensitive(headers, "Content-Type", "application/json");
|
|
541
|
+
setHeaderCaseInsensitive(headers, "Authorization", `Bearer ${apiKey}`);
|
|
550
542
|
return { method: "POST", headers, body: JSON.stringify(body), signal };
|
|
551
543
|
}
|
|
552
544
|
|
|
@@ -558,13 +550,18 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
558
550
|
sessionId: string | undefined,
|
|
559
551
|
signal?: AbortSignal
|
|
560
552
|
): Promise<{ status: number; statusText: string; headers: Record<string, string>; text: string }> {
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
headers
|
|
566
|
-
|
|
567
|
-
|
|
553
|
+
const { signal: effective, cleanup } = combinedSignal(signal, timeoutFor(options as { timeoutMs?: number } | undefined));
|
|
554
|
+
try {
|
|
555
|
+
const res = await fetch(url, this.requestInit(body, options, apiKey, sessionId, effective));
|
|
556
|
+
const text = await res.text();
|
|
557
|
+
const headers: Record<string, string> = {};
|
|
558
|
+
res.headers.forEach((v, k) => {
|
|
559
|
+
headers[k] = v;
|
|
560
|
+
});
|
|
561
|
+
return { status: res.status, statusText: res.statusText, headers, text };
|
|
562
|
+
} finally {
|
|
563
|
+
cleanup();
|
|
564
|
+
}
|
|
568
565
|
}
|
|
569
566
|
|
|
570
567
|
private throwIfError(
|
|
@@ -728,7 +725,22 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
728
725
|
body,
|
|
729
726
|
};
|
|
730
727
|
|
|
731
|
-
|
|
728
|
+
// Q-17: optional per-call timeout races the linked abort signal.
|
|
729
|
+
const streamTimeoutMs = timeoutFor(options as { timeoutMs?: number } | undefined);
|
|
730
|
+
const streamTimeoutTimer =
|
|
731
|
+
streamTimeoutMs && streamTimeoutMs > 0
|
|
732
|
+
? setTimeout(() => {
|
|
733
|
+
try {
|
|
734
|
+
linked.abort();
|
|
735
|
+
} catch {}
|
|
736
|
+
}, streamTimeoutMs)
|
|
737
|
+
: undefined;
|
|
738
|
+
let res: Response;
|
|
739
|
+
try {
|
|
740
|
+
res = await fetch(url, this.requestInit(body, options, apiKey, sessionId, linked.signal));
|
|
741
|
+
} finally {
|
|
742
|
+
if (streamTimeoutTimer) clearTimeout(streamTimeoutTimer);
|
|
743
|
+
}
|
|
732
744
|
if (!res.ok || !res.body) {
|
|
733
745
|
const text = !res.ok ? await res.text().catch(() => "") : "";
|
|
734
746
|
if (!res.ok) this.throwIfError(res.status, url, text, clean);
|
|
@@ -738,7 +750,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
738
750
|
res.headers.forEach((v, k) => {
|
|
739
751
|
responseHeaders[k] = v;
|
|
740
752
|
});
|
|
741
|
-
const responseMeta = { status: res.status, statusText: res.statusText, headers: responseHeaders };
|
|
753
|
+
const responseMeta: Record<string, unknown> & { status: number; statusText: string; headers: Record<string, string> } = { status: res.status, statusText: res.statusText, headers: responseHeaders };
|
|
742
754
|
eventStream.push({ type: "start", raw: { request: rawRequest } } as never);
|
|
743
755
|
|
|
744
756
|
const parser = new SSEParser();
|
|
@@ -750,6 +762,11 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
750
762
|
const calls = new Map<number, { itemId: string; callId: string; name: string; startArgs: string; deltaArgs: string }>();
|
|
751
763
|
let usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
752
764
|
let finishReason = "stop";
|
|
765
|
+
let sawFinishReason = false;
|
|
766
|
+
let sawUsage = false;
|
|
767
|
+
let refusalSeen = false;
|
|
768
|
+
let badFrames = 0;
|
|
769
|
+
let framesSeen = 0;
|
|
753
770
|
let responseId: string | undefined;
|
|
754
771
|
let completedBody: unknown = undefined;
|
|
755
772
|
let aborted = false;
|
|
@@ -765,11 +782,16 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
765
782
|
);
|
|
766
783
|
|
|
767
784
|
const handleMessage = (data: string): void => {
|
|
768
|
-
if (!data || data === "[DONE]")
|
|
785
|
+
if (!data || data === "[DONE]") {
|
|
786
|
+
if (data === "[DONE]") sawFinishReason = sawFinishReason || completedBody !== undefined || sawUsage;
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
769
789
|
let msg: Record<string, unknown>;
|
|
770
790
|
try {
|
|
771
791
|
msg = JSON.parse(data) as Record<string, unknown>;
|
|
792
|
+
framesSeen += 1;
|
|
772
793
|
} catch {
|
|
794
|
+
badFrames += 1;
|
|
773
795
|
return;
|
|
774
796
|
}
|
|
775
797
|
const type = msg["type"] as string;
|
|
@@ -790,6 +812,17 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
790
812
|
} else if (type === "response.output_text.delta" && typeof msg["delta"] === "string") {
|
|
791
813
|
text += msg["delta"] as string;
|
|
792
814
|
eventStream.push({ type: "text_delta", delta: msg["delta"] as string, partialText: text });
|
|
815
|
+
} else if (
|
|
816
|
+
type === "response.refusal.delta" ||
|
|
817
|
+
type === "response.output_refusal.delta"
|
|
818
|
+
) {
|
|
819
|
+
// Q-23: refusal deltas carry the filter text; surface as text and
|
|
820
|
+
// remember for a `refusal` finish reason.
|
|
821
|
+
if (typeof msg["delta"] === "string" && msg["delta"]) {
|
|
822
|
+
text += msg["delta"] as string;
|
|
823
|
+
refusalSeen = true;
|
|
824
|
+
eventStream.push({ type: "text_delta", delta: msg["delta"] as string, partialText: text });
|
|
825
|
+
}
|
|
793
826
|
} else if (
|
|
794
827
|
(type === "response.reasoning_text.delta" || type === "response.reasoning.delta") &&
|
|
795
828
|
typeof msg["delta"] === "string"
|
|
@@ -827,7 +860,11 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
827
860
|
const response = (msg["response"] as ResponsesObject) ?? {};
|
|
828
861
|
completedBody = msg["response"];
|
|
829
862
|
if (response.id) responseId = response.id;
|
|
830
|
-
if (response.usage)
|
|
863
|
+
if (response.usage) {
|
|
864
|
+
usage = mapUsage(response.usage);
|
|
865
|
+
sawUsage = true;
|
|
866
|
+
}
|
|
867
|
+
sawFinishReason = true;
|
|
831
868
|
if (type === "response.failed" || response.status === "failed") {
|
|
832
869
|
const message =
|
|
833
870
|
(response.error && typeof response.error.message === "string" && response.error.message) ||
|
|
@@ -839,7 +876,12 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
839
876
|
}
|
|
840
877
|
// Non-streaming maps `incomplete` → `length` (max tokens). Streaming
|
|
841
878
|
// must do the same — otherwise truncated turns misreport `stop`.
|
|
842
|
-
|
|
879
|
+
// Q-23: content-filter incompletes surface as `refusal`.
|
|
880
|
+
const incompleteReason = (response as { incomplete_details?: { reason?: string } }).incomplete_details?.reason;
|
|
881
|
+
if (response.status === "incomplete") {
|
|
882
|
+
finishReason = incompleteReason === "content_filter" ? "refusal" : "length";
|
|
883
|
+
sawFinishReason = true;
|
|
884
|
+
}
|
|
843
885
|
// Merge any full tool items delivered at completion (authoritative
|
|
844
886
|
// when present) so nothing depends solely on delta assembly.
|
|
845
887
|
for (const item of response.output ?? []) {
|
|
@@ -876,6 +918,17 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
876
918
|
}
|
|
877
919
|
}
|
|
878
920
|
}
|
|
921
|
+
// Merge completed refusal text (filter truncations often arrive only
|
|
922
|
+
// here with no preceding refusal delta).
|
|
923
|
+
for (const item of response.output ?? []) {
|
|
924
|
+
if (item.type !== "message") continue;
|
|
925
|
+
for (const block of item.content ?? []) {
|
|
926
|
+
if (block.type === "refusal" && block.text) {
|
|
927
|
+
if (!text.includes(block.text)) text += block.text;
|
|
928
|
+
refusalSeen = true;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
879
932
|
eventStream.push({ type: "usage", usage });
|
|
880
933
|
}
|
|
881
934
|
};
|
|
@@ -905,7 +958,7 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
905
958
|
for (const c of calls.values()) {
|
|
906
959
|
const args = parseStreamedToolArguments(c.startArgs, c.deltaArgs);
|
|
907
960
|
const record: ToolCallRecord = {
|
|
908
|
-
id: c.itemId || c.callId ||
|
|
961
|
+
id: c.itemId || c.callId || newToolCallId(),
|
|
909
962
|
callId: c.callId || undefined,
|
|
910
963
|
name: c.name,
|
|
911
964
|
arguments: args,
|
|
@@ -916,9 +969,33 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
916
969
|
}
|
|
917
970
|
|
|
918
971
|
noteProviderTurn(sessionId, "openai");
|
|
919
|
-
if (toolCalls.length > 0)
|
|
972
|
+
if (toolCalls.length > 0) {
|
|
973
|
+
finishReason = "tool_calls";
|
|
974
|
+
sawFinishReason = true;
|
|
975
|
+
} else if (refusalSeen && finishReason !== "length") {
|
|
976
|
+
finishReason = "refusal";
|
|
977
|
+
sawFinishReason = true;
|
|
978
|
+
}
|
|
920
979
|
|
|
921
|
-
|
|
980
|
+
// Q-23: terminal-state guard. Loops ending with no completed body, no
|
|
981
|
+
// finish_reason, and no usage while carrying no content must fail as
|
|
982
|
+
// `incomplete_stream` (retryable) instead of resolving empty success.
|
|
983
|
+
// Skipped for non-SSE payloads (e.g. JSON mocks in delegation tests)
|
|
984
|
+
// where no SSE frames were ever seen — those preserve legacy empty
|
|
985
|
+
// success rather than failing the caller on a mock artifact.
|
|
986
|
+
const hasTerminal = completedBody !== undefined || sawFinishReason || sawUsage;
|
|
987
|
+
const cleanThinkingEarly = thinking.replace(/\n{3,}/g, "\n\n").trim();
|
|
988
|
+
const ctEntry = Object.entries(responseHeaders).find(([k]) => k.toLowerCase() === "content-type");
|
|
989
|
+
const isSSEPayload =
|
|
990
|
+
(typeof ctEntry?.[1] === "string" && ((ctEntry[1] as string).includes("event-stream") || (ctEntry[1] as string).startsWith("text/"))) ||
|
|
991
|
+
framesSeen > 0 ||
|
|
992
|
+
badFrames > 0;
|
|
993
|
+
if (!hasTerminal && !text && !cleanThinkingEarly && toolCalls.length === 0 && isSSEPayload) {
|
|
994
|
+
eventStream.fail(incompleteStreamError("openai", clean));
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const cleanThinking = cleanThinkingEarly;
|
|
922
999
|
const finalResponse = new AgentResponse({
|
|
923
1000
|
text,
|
|
924
1001
|
thinking: cleanThinking || undefined,
|
|
@@ -929,7 +1006,10 @@ export class OpenAIResponsesProvider implements Provider {
|
|
|
929
1006
|
responseId,
|
|
930
1007
|
model: clean,
|
|
931
1008
|
provider: "openai",
|
|
932
|
-
raw: {
|
|
1009
|
+
raw: {
|
|
1010
|
+
request: rawRequest,
|
|
1011
|
+
response: { ...responseMeta, body: completedBody, badFrames } as unknown as ProviderRawData["response"],
|
|
1012
|
+
},
|
|
933
1013
|
durationMs: Date.now() - startTime,
|
|
934
1014
|
});
|
|
935
1015
|
eventStream.push({ type: "done", delta: "", usage, finishReason, responseId });
|