@centerforagenticai/pi-multi-account 0.1.1
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/LICENSE +21 -0
- package/NOTICE +29 -0
- package/README.md +999 -0
- package/config/models/pi-multi-account.v1.json +32 -0
- package/config/subscription-plans.v1.json +122 -0
- package/package.json +76 -0
- package/packages/pi-anthropic-oauth/LICENSE +21 -0
- package/packages/pi-anthropic-oauth/package.json +54 -0
- package/packages/pi-anthropic-oauth/src/auth.ts +396 -0
- package/packages/pi-anthropic-oauth/src/context.ts +116 -0
- package/packages/pi-anthropic-oauth/src/convert.ts +303 -0
- package/packages/pi-anthropic-oauth/src/index.ts +37 -0
- package/packages/pi-anthropic-oauth/src/prompt.ts +137 -0
- package/packages/pi-anthropic-oauth/src/stream.ts +476 -0
- package/packages/pi-antigravity/LICENSE +21 -0
- package/packages/pi-antigravity/package.json +77 -0
- package/packages/pi-antigravity/src/auth/index.ts +14 -0
- package/packages/pi-antigravity/src/auth/oauth.ts +442 -0
- package/packages/pi-antigravity/src/client/client.ts +561 -0
- package/packages/pi-antigravity/src/client/index.ts +1 -0
- package/packages/pi-antigravity/src/context.ts +110 -0
- package/packages/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
- package/packages/pi-antigravity/src/diagnostics/index.ts +1 -0
- package/packages/pi-antigravity/src/image/image.ts +336 -0
- package/packages/pi-antigravity/src/image/index.ts +1 -0
- package/packages/pi-antigravity/src/index.ts +280 -0
- package/packages/pi-antigravity/src/models/discovery.ts +154 -0
- package/packages/pi-antigravity/src/models/grouping.ts +424 -0
- package/packages/pi-antigravity/src/models/index.ts +3 -0
- package/packages/pi-antigravity/src/models/models.ts +500 -0
- package/packages/pi-antigravity/src/stream/index.ts +1 -0
- package/packages/pi-antigravity/src/stream/stream.ts +1478 -0
- package/packages/pi-antigravity/src/types/enums.ts +42 -0
- package/packages/pi-antigravity/src/types/index.ts +2 -0
- package/packages/pi-antigravity/src/types/types.ts +292 -0
- package/packages/pi-antigravity/src/usage/index.ts +1 -0
- package/packages/pi-antigravity/src/usage/usage.ts +416 -0
- package/packages/pi-antigravity/src/utils/http.ts +91 -0
- package/packages/pi-antigravity/src/utils/index.ts +3 -0
- package/packages/pi-antigravity/src/utils/security.ts +73 -0
- package/packages/pi-antigravity/src/utils/util.ts +132 -0
- package/scripts/multi-account.mjs +44 -0
- package/src/account-labels.ts +223 -0
- package/src/account-plan-assignment.ts +340 -0
- package/src/account-rate-history.ts +372 -0
- package/src/anthropic-adaptive-stream.ts +531 -0
- package/src/anthropic-alias-stream.ts +140 -0
- package/src/anthropic-context-compat.ts +80 -0
- package/src/api-pricing.ts +579 -0
- package/src/bounded-file-lines.ts +97 -0
- package/src/catalog-rebinding.ts +177 -0
- package/src/catalog-registration-probe.ts +111 -0
- package/src/codex-adapter.ts +345 -0
- package/src/codex-model-defaults.ts +785 -0
- package/src/command-completions.ts +404 -0
- package/src/commands.ts +2000 -0
- package/src/compaction.ts +14 -0
- package/src/config.ts +1317 -0
- package/src/continuation.ts +569 -0
- package/src/cooldowns.ts +110 -0
- package/src/cost-digest-store.ts +332 -0
- package/src/cost-digest.ts +1044 -0
- package/src/cost-history.ts +251 -0
- package/src/cost-period-closer.ts +160 -0
- package/src/cost-report-json.ts +318 -0
- package/src/cost-report-reader.ts +368 -0
- package/src/cost-report-render.ts +207 -0
- package/src/cost-report.ts +1104 -0
- package/src/coverage-attestation.ts +397 -0
- package/src/credential-lifecycle.ts +169 -0
- package/src/credential-refresh.ts +248 -0
- package/src/declaration-notice-marker.ts +238 -0
- package/src/diagnostic-store.ts +276 -0
- package/src/diagnostics.ts +309 -0
- package/src/discovery.ts +471 -0
- package/src/duration.ts +13 -0
- package/src/error-classification.ts +256 -0
- package/src/fuzzy.ts +15 -0
- package/src/group-policy.ts +81 -0
- package/src/history-store.ts +897 -0
- package/src/index.ts +5572 -0
- package/src/lifecycle.ts +378 -0
- package/src/logical-dispatch.ts +279 -0
- package/src/logical-model-selector.ts +254 -0
- package/src/logical-model-switcher.ts +430 -0
- package/src/logical-provider-attribution.ts +544 -0
- package/src/logical-provider.ts +1237 -0
- package/src/logical-route-indicator.ts +215 -0
- package/src/machine-lease.ts +445 -0
- package/src/model-support.ts +66 -0
- package/src/models-declaration.ts +1091 -0
- package/src/openai-adapter.ts +117 -0
- package/src/openrouter-budget.ts +304 -0
- package/src/openrouter-fallback.ts +146 -0
- package/src/period-boundaries.ts +376 -0
- package/src/pi-anthropic-oauth.d.ts +6 -0
- package/src/preflight.ts +253 -0
- package/src/pricing-cache.ts +235 -0
- package/src/project-identity.ts +100 -0
- package/src/provider-registration.ts +942 -0
- package/src/rate-formula.ts +163 -0
- package/src/recovery-engine.ts +853 -0
- package/src/recovery-output.ts +837 -0
- package/src/recovery-plan.ts +239 -0
- package/src/report-range.ts +203 -0
- package/src/route-resolver.ts +789 -0
- package/src/routing-config-transaction.ts +232 -0
- package/src/routing.ts +1163 -0
- package/src/runtime-state.ts +630 -0
- package/src/session-account-groups.ts +284 -0
- package/src/session-restore.ts +287 -0
- package/src/shared-usage.ts +1392 -0
- package/src/standalone-cli.ts +720 -0
- package/src/status-view.ts +578 -0
- package/src/subscription-plan-catalog.ts +346 -0
- package/src/tier-model-resolver.ts +46 -0
- package/src/upstream-anthropic.ts +315 -0
- package/src/upstream-antigravity.ts +327 -0
- package/src/usage-fetch.ts +1634 -0
- package/src/usage.ts +1026 -0
- package/src/vendor.ts +87 -0
- package/src/warmer.ts +231 -0
- package/src/watchdog.ts +219 -0
- package/src/window-history.ts +270 -0
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AssistantMessage,
|
|
3
|
+
AssistantMessageEvent,
|
|
4
|
+
AssistantMessageEventStream,
|
|
5
|
+
ToolCall,
|
|
6
|
+
} from "@earendil-works/pi-ai";
|
|
7
|
+
|
|
8
|
+
export interface AcceptedOutputLimits {
|
|
9
|
+
readonly maxBufferedBytes: number;
|
|
10
|
+
readonly maxEvents: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Engineering caps for one private physical attempt.
|
|
15
|
+
*
|
|
16
|
+
* The pinned 0.84.4 catalog and repository fixtures advertise at most 128,000
|
|
17
|
+
* output tokens. The 24 MiB byte cap leaves 64 UTF-8 bytes per advertised token
|
|
18
|
+
* for each of the exact terminal, immutable accepted snapshot, and reconstructed
|
|
19
|
+
* deltas. The 262,144-event cap allows two wire events per advertised token.
|
|
20
|
+
* These are explicit safety margins, not measurements of every possible model
|
|
21
|
+
* response. The adapter counts bytes and events while consuming, but retains
|
|
22
|
+
* neither producer events nor their cumulative mutable `partial` objects. A
|
|
23
|
+
* response outside either engineering cap fails in full.
|
|
24
|
+
*/
|
|
25
|
+
export const ACCEPTED_OUTPUT_DEFAULT_LIMITS: AcceptedOutputLimits = Object.freeze({
|
|
26
|
+
maxBufferedBytes: 24 * 1024 * 1024,
|
|
27
|
+
maxEvents: 262_144,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Give async producer finally blocks one bounded second; timeout fails the
|
|
31
|
+
// private attempt without publishing content.
|
|
32
|
+
const TERMINAL_CLEANUP_TIMEOUT_MS = 1_000;
|
|
33
|
+
|
|
34
|
+
export type AcceptedOutputErrorCode =
|
|
35
|
+
| "aborted"
|
|
36
|
+
| "malformed"
|
|
37
|
+
| "overflow"
|
|
38
|
+
| "rejected";
|
|
39
|
+
|
|
40
|
+
const ERROR_MESSAGES: Readonly<Record<AcceptedOutputErrorCode, string>> = Object.freeze({
|
|
41
|
+
aborted: "The candidate output was aborted before acceptance.",
|
|
42
|
+
malformed: "The candidate output did not satisfy the accepted stream contract.",
|
|
43
|
+
overflow: "The candidate output exceeded the accepted output limit.",
|
|
44
|
+
rejected: "The candidate output was rejected before acceptance.",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** A content-free failure safe to expose outside the private attempt. */
|
|
48
|
+
export class AcceptedOutputError extends Error {
|
|
49
|
+
readonly code: AcceptedOutputErrorCode;
|
|
50
|
+
|
|
51
|
+
constructor(code: AcceptedOutputErrorCode) {
|
|
52
|
+
super(ERROR_MESSAGES[code]);
|
|
53
|
+
this.name = "AcceptedOutputError";
|
|
54
|
+
this.code = code;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AcceptedOutputOptions extends Partial<AcceptedOutputLimits> {
|
|
59
|
+
readonly signal?: AbortSignal;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type AttemptState =
|
|
63
|
+
| { readonly status: "pending" }
|
|
64
|
+
| {
|
|
65
|
+
readonly status: "accepted";
|
|
66
|
+
readonly terminal: AssistantMessage;
|
|
67
|
+
readonly partial: AssistantMessage | undefined;
|
|
68
|
+
}
|
|
69
|
+
| { readonly status: "failed"; readonly error: AcceptedOutputError };
|
|
70
|
+
|
|
71
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
72
|
+
return typeof value === "object" && value !== null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
76
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isUsage(value: unknown): value is AssistantMessage["usage"] {
|
|
80
|
+
if (!isRecord(value) || !isRecord(value.cost)) return false;
|
|
81
|
+
return (
|
|
82
|
+
isFiniteNumber(value.input) &&
|
|
83
|
+
isFiniteNumber(value.output) &&
|
|
84
|
+
isFiniteNumber(value.cacheRead) &&
|
|
85
|
+
isFiniteNumber(value.cacheWrite) &&
|
|
86
|
+
isFiniteNumber(value.totalTokens) &&
|
|
87
|
+
isFiniteNumber(value.cost.input) &&
|
|
88
|
+
isFiniteNumber(value.cost.output) &&
|
|
89
|
+
isFiniteNumber(value.cost.cacheRead) &&
|
|
90
|
+
isFiniteNumber(value.cost.cacheWrite) &&
|
|
91
|
+
isFiniteNumber(value.cost.total)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isJsonValue(value: unknown, ancestors = new Set<object>()): boolean {
|
|
96
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
97
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
98
|
+
if (typeof value !== "object" || ancestors.has(value)) return false;
|
|
99
|
+
ancestors.add(value);
|
|
100
|
+
const valid = Array.isArray(value)
|
|
101
|
+
? value.every((item, index) => index in value && isJsonValue(item, ancestors)) &&
|
|
102
|
+
Object.keys(value).length === value.length
|
|
103
|
+
: (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) &&
|
|
104
|
+
Object.values(value).every((item) => isJsonValue(item, ancestors));
|
|
105
|
+
ancestors.delete(value);
|
|
106
|
+
return valid;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isAssistantMessageEnvelope(value: unknown): value is AssistantMessage {
|
|
110
|
+
return (
|
|
111
|
+
isRecord(value) &&
|
|
112
|
+
value.role === "assistant" &&
|
|
113
|
+
Array.isArray(value.content) &&
|
|
114
|
+
typeof value.api === "string" &&
|
|
115
|
+
typeof value.provider === "string" &&
|
|
116
|
+
typeof value.model === "string" &&
|
|
117
|
+
typeof value.stopReason === "string" &&
|
|
118
|
+
isFiniteNumber(value.timestamp) &&
|
|
119
|
+
isUsage(value.usage)
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isAssistantMessage(value: unknown): value is AssistantMessage {
|
|
124
|
+
if (!isAssistantMessageEnvelope(value)) return false;
|
|
125
|
+
return value.content.every((block) => {
|
|
126
|
+
if (!isRecord(block) || typeof block.type !== "string") return false;
|
|
127
|
+
if (block.type === "text") {
|
|
128
|
+
return (
|
|
129
|
+
typeof block.text === "string" &&
|
|
130
|
+
(block.textSignature === undefined || typeof block.textSignature === "string")
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (block.type === "thinking") {
|
|
134
|
+
return (
|
|
135
|
+
typeof block.thinking === "string" &&
|
|
136
|
+
(block.thinkingSignature === undefined || typeof block.thinkingSignature === "string") &&
|
|
137
|
+
(block.redacted === undefined || typeof block.redacted === "boolean")
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return (
|
|
141
|
+
block.type === "toolCall" &&
|
|
142
|
+
typeof block.id === "string" &&
|
|
143
|
+
typeof block.name === "string" &&
|
|
144
|
+
isRecord(block.arguments) &&
|
|
145
|
+
isJsonValue(block.arguments) &&
|
|
146
|
+
(block.thoughtSignature === undefined || typeof block.thoughtSignature === "string") &&
|
|
147
|
+
(block.namespace === undefined || typeof block.namespace === "string")
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isContentIndex(value: unknown): value is number {
|
|
153
|
+
return Number.isSafeInteger(value) && (value as number) >= 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isPartialEventEnvelope(value: Record<string, unknown>): boolean {
|
|
157
|
+
return isContentIndex(value.contentIndex) && isAssistantMessageEnvelope(value.partial);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Checks only the event envelope while consuming. Producer partials are
|
|
162
|
+
* cumulative mutable snapshots, so recursively walking one for every delta
|
|
163
|
+
* would make a growing response quadratic. The authoritative terminal is
|
|
164
|
+
* deeply validated once after producer cleanup and before any clone.
|
|
165
|
+
*/
|
|
166
|
+
function isAssistantMessageEventEnvelope(value: unknown): value is AssistantMessageEvent {
|
|
167
|
+
if (!isRecord(value) || typeof value.type !== "string") return false;
|
|
168
|
+
switch (value.type) {
|
|
169
|
+
case "start":
|
|
170
|
+
return isAssistantMessageEnvelope(value.partial);
|
|
171
|
+
case "text_start":
|
|
172
|
+
case "thinking_start":
|
|
173
|
+
case "toolcall_start":
|
|
174
|
+
return isPartialEventEnvelope(value);
|
|
175
|
+
case "text_delta":
|
|
176
|
+
case "thinking_delta":
|
|
177
|
+
case "toolcall_delta":
|
|
178
|
+
return isPartialEventEnvelope(value) && typeof value.delta === "string";
|
|
179
|
+
case "text_end":
|
|
180
|
+
case "thinking_end":
|
|
181
|
+
return isPartialEventEnvelope(value) && typeof value.content === "string";
|
|
182
|
+
case "toolcall_end":
|
|
183
|
+
return (
|
|
184
|
+
isPartialEventEnvelope(value) &&
|
|
185
|
+
isRecord(value.toolCall) &&
|
|
186
|
+
value.toolCall.type === "toolCall" &&
|
|
187
|
+
typeof value.toolCall.id === "string" &&
|
|
188
|
+
typeof value.toolCall.name === "string" &&
|
|
189
|
+
isRecord(value.toolCall.arguments)
|
|
190
|
+
);
|
|
191
|
+
case "done":
|
|
192
|
+
return (
|
|
193
|
+
(value.reason === "stop" ||
|
|
194
|
+
value.reason === "length" ||
|
|
195
|
+
value.reason === "toolUse" ||
|
|
196
|
+
value.reason === "deferred") &&
|
|
197
|
+
isAssistantMessageEnvelope(value.message) &&
|
|
198
|
+
value.message.stopReason === value.reason
|
|
199
|
+
);
|
|
200
|
+
case "error":
|
|
201
|
+
return (
|
|
202
|
+
(value.reason === "error" || value.reason === "aborted") &&
|
|
203
|
+
isAssistantMessageEnvelope(value.error)
|
|
204
|
+
);
|
|
205
|
+
default:
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function assertLimits(options: AcceptedOutputOptions): AcceptedOutputLimits {
|
|
211
|
+
const maxBufferedBytes =
|
|
212
|
+
options.maxBufferedBytes ?? ACCEPTED_OUTPUT_DEFAULT_LIMITS.maxBufferedBytes;
|
|
213
|
+
const maxEvents = options.maxEvents ?? ACCEPTED_OUTPUT_DEFAULT_LIMITS.maxEvents;
|
|
214
|
+
if (
|
|
215
|
+
!Number.isSafeInteger(maxBufferedBytes) ||
|
|
216
|
+
maxBufferedBytes <= 0 ||
|
|
217
|
+
!Number.isSafeInteger(maxEvents) ||
|
|
218
|
+
maxEvents <= 0
|
|
219
|
+
) {
|
|
220
|
+
throw new TypeError("Accepted output limits must be positive safe integers.");
|
|
221
|
+
}
|
|
222
|
+
return { maxBufferedBytes, maxEvents };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function measuredBytes(value: unknown, limit: number): number {
|
|
226
|
+
const ancestors = new Set<object>();
|
|
227
|
+
const visit = (candidate: unknown, total: number): number => {
|
|
228
|
+
if (total > limit) return total;
|
|
229
|
+
if (candidate === null) return total + 4;
|
|
230
|
+
switch (typeof candidate) {
|
|
231
|
+
case "string":
|
|
232
|
+
return total + Buffer.byteLength(candidate, "utf8");
|
|
233
|
+
case "number":
|
|
234
|
+
return total + 16;
|
|
235
|
+
case "boolean":
|
|
236
|
+
return total + 5;
|
|
237
|
+
case "undefined":
|
|
238
|
+
return total;
|
|
239
|
+
case "object": {
|
|
240
|
+
if (ancestors.has(candidate)) throw new AcceptedOutputError("malformed");
|
|
241
|
+
ancestors.add(candidate);
|
|
242
|
+
let measured = total;
|
|
243
|
+
if (Array.isArray(candidate)) {
|
|
244
|
+
for (const item of candidate) {
|
|
245
|
+
measured = visit(item, measured + 1);
|
|
246
|
+
if (measured > limit) break;
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
for (const [key, item] of Object.entries(candidate)) {
|
|
250
|
+
measured += Buffer.byteLength(key, "utf8") + 1;
|
|
251
|
+
measured = visit(item, measured);
|
|
252
|
+
if (measured > limit) break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
ancestors.delete(candidate);
|
|
256
|
+
return measured;
|
|
257
|
+
}
|
|
258
|
+
default:
|
|
259
|
+
throw new AcceptedOutputError("malformed");
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
return visit(value, 0);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function jsonText(value: unknown): string {
|
|
266
|
+
if (value === null) return "null";
|
|
267
|
+
if (typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
|
|
268
|
+
if (typeof value === "number") return Object.is(value, -0) ? "-0" : String(value);
|
|
269
|
+
if (Array.isArray(value)) {
|
|
270
|
+
const items: string[] = [];
|
|
271
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
272
|
+
if (!(index in value)) throw new AcceptedOutputError("malformed");
|
|
273
|
+
items.push(jsonText(value[index]));
|
|
274
|
+
}
|
|
275
|
+
return `[${items.join(",")}]`;
|
|
276
|
+
}
|
|
277
|
+
if (isRecord(value)) {
|
|
278
|
+
return `{${Object.entries(value)
|
|
279
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${jsonText(item)}`)
|
|
280
|
+
.join(",")}}`;
|
|
281
|
+
}
|
|
282
|
+
throw new AcceptedOutputError("malformed");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function jsonStringBytes(value: string): number {
|
|
286
|
+
let bytes = 2;
|
|
287
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
288
|
+
const unit = value.charCodeAt(index);
|
|
289
|
+
if (unit === 0x22 || unit === 0x5c) {
|
|
290
|
+
bytes += 2;
|
|
291
|
+
} else if (unit <= 0x1f) {
|
|
292
|
+
bytes += unit === 0x08 || unit === 0x09 || unit === 0x0a || unit === 0x0c || unit === 0x0d
|
|
293
|
+
? 2
|
|
294
|
+
: 6;
|
|
295
|
+
} else if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
296
|
+
const following = value.charCodeAt(index + 1);
|
|
297
|
+
if (following >= 0xdc00 && following <= 0xdfff) {
|
|
298
|
+
bytes += 4;
|
|
299
|
+
index += 1;
|
|
300
|
+
} else {
|
|
301
|
+
bytes += 6;
|
|
302
|
+
}
|
|
303
|
+
} else if (unit >= 0xdc00 && unit <= 0xdfff) {
|
|
304
|
+
bytes += 6;
|
|
305
|
+
} else if (unit <= 0x7f) {
|
|
306
|
+
bytes += 1;
|
|
307
|
+
} else if (unit <= 0x7ff) {
|
|
308
|
+
bytes += 2;
|
|
309
|
+
} else {
|
|
310
|
+
bytes += 3;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return bytes;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function jsonTextBytes(value: unknown, limit: number): number {
|
|
317
|
+
if (value === null) return 4;
|
|
318
|
+
if (typeof value === "string") return jsonStringBytes(value);
|
|
319
|
+
if (typeof value === "boolean") return value ? 4 : 5;
|
|
320
|
+
if (typeof value === "number") return Object.is(value, -0) ? 2 : String(value).length;
|
|
321
|
+
let bytes = 2;
|
|
322
|
+
if (Array.isArray(value)) {
|
|
323
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
324
|
+
if (!(index in value)) throw new AcceptedOutputError("malformed");
|
|
325
|
+
if (index > 0) bytes += 1;
|
|
326
|
+
bytes += jsonTextBytes(value[index], limit - bytes);
|
|
327
|
+
if (bytes > limit) break;
|
|
328
|
+
}
|
|
329
|
+
return bytes;
|
|
330
|
+
}
|
|
331
|
+
if (isRecord(value)) {
|
|
332
|
+
let index = 0;
|
|
333
|
+
for (const [key, item] of Object.entries(value)) {
|
|
334
|
+
if (index > 0) bytes += 1;
|
|
335
|
+
bytes += jsonStringBytes(key) + 1;
|
|
336
|
+
bytes += jsonTextBytes(item, limit - bytes);
|
|
337
|
+
if (bytes > limit) break;
|
|
338
|
+
index += 1;
|
|
339
|
+
}
|
|
340
|
+
return bytes;
|
|
341
|
+
}
|
|
342
|
+
throw new AcceptedOutputError("malformed");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function deltaBytes(event: AssistantMessageEvent): number {
|
|
346
|
+
switch (event.type) {
|
|
347
|
+
case "text_delta":
|
|
348
|
+
case "thinking_delta":
|
|
349
|
+
case "toolcall_delta":
|
|
350
|
+
return Buffer.byteLength(event.delta, "utf8");
|
|
351
|
+
default:
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function copyUsage(usage: AssistantMessage["usage"]): AssistantMessage["usage"] {
|
|
357
|
+
return {
|
|
358
|
+
input: usage.input,
|
|
359
|
+
output: usage.output,
|
|
360
|
+
cacheRead: usage.cacheRead,
|
|
361
|
+
cacheWrite: usage.cacheWrite,
|
|
362
|
+
...(usage.cacheWrite1h === undefined ? {} : { cacheWrite1h: usage.cacheWrite1h }),
|
|
363
|
+
...(usage.reasoning === undefined ? {} : { reasoning: usage.reasoning }),
|
|
364
|
+
totalTokens: usage.totalTokens,
|
|
365
|
+
cost: {
|
|
366
|
+
input: usage.cost.input,
|
|
367
|
+
output: usage.cost.output,
|
|
368
|
+
cacheRead: usage.cost.cacheRead,
|
|
369
|
+
cacheWrite: usage.cost.cacheWrite,
|
|
370
|
+
total: usage.cost.total,
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function copyPartialMessage(
|
|
376
|
+
terminal: AssistantMessage,
|
|
377
|
+
content: AssistantMessage["content"],
|
|
378
|
+
): AssistantMessage {
|
|
379
|
+
return {
|
|
380
|
+
role: "assistant",
|
|
381
|
+
content,
|
|
382
|
+
api: terminal.api,
|
|
383
|
+
provider: terminal.provider,
|
|
384
|
+
model: terminal.model,
|
|
385
|
+
...(terminal.responseModel === undefined ? {} : { responseModel: terminal.responseModel }),
|
|
386
|
+
...(terminal.responseId === undefined ? {} : { responseId: terminal.responseId }),
|
|
387
|
+
usage: copyUsage(terminal.usage),
|
|
388
|
+
stopReason: "pending",
|
|
389
|
+
timestamp: terminal.timestamp,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function deepFreeze(value: unknown, seen = new Set<object>()): void {
|
|
394
|
+
if (typeof value !== "object" || value === null || seen.has(value)) return;
|
|
395
|
+
seen.add(value);
|
|
396
|
+
for (const child of Object.values(value)) deepFreeze(child, seen);
|
|
397
|
+
Object.freeze(value);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function acceptedPartial(terminal: AssistantMessage): AssistantMessage {
|
|
401
|
+
const content = structuredClone(terminal.content);
|
|
402
|
+
const partial = copyPartialMessage(terminal, content);
|
|
403
|
+
deepFreeze(partial);
|
|
404
|
+
return partial;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function projectedAcceptedBytes(
|
|
408
|
+
terminal: AssistantMessage,
|
|
409
|
+
limit: number,
|
|
410
|
+
): number {
|
|
411
|
+
let total = measuredBytes(terminal, limit);
|
|
412
|
+
total += measuredBytes(copyPartialMessage(terminal, terminal.content), limit - total);
|
|
413
|
+
total += measuredBytes(copyPartialMessage(terminal, []), limit - total);
|
|
414
|
+
for (const block of terminal.content) {
|
|
415
|
+
total += block.type === "text"
|
|
416
|
+
? Buffer.byteLength(block.text, "utf8")
|
|
417
|
+
: block.type === "thinking"
|
|
418
|
+
? Buffer.byteLength(block.thinking, "utf8")
|
|
419
|
+
: jsonTextBytes(block.arguments, limit - total);
|
|
420
|
+
if (total > limit) break;
|
|
421
|
+
}
|
|
422
|
+
return total;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Replays a newly constructed accepted sequence. Delta consumers can rebuild
|
|
427
|
+
* every block from the emitted deltas, while snapshot consumers see one stable,
|
|
428
|
+
* immutable accepted snapshot rather than retained mutable producer partials.
|
|
429
|
+
* The terminal event and `result()` retain the exact accepted object.
|
|
430
|
+
*/
|
|
431
|
+
async function* acceptedEvents(
|
|
432
|
+
terminal: AssistantMessage,
|
|
433
|
+
partial: AssistantMessage,
|
|
434
|
+
): AsyncGenerator<AssistantMessageEvent> {
|
|
435
|
+
const start = copyPartialMessage(terminal, []);
|
|
436
|
+
deepFreeze(start);
|
|
437
|
+
yield { type: "start", partial: start };
|
|
438
|
+
|
|
439
|
+
for (let contentIndex = 0; contentIndex < partial.content.length; contentIndex += 1) {
|
|
440
|
+
const accepted = partial.content[contentIndex]!;
|
|
441
|
+
if (accepted.type === "text") {
|
|
442
|
+
yield { type: "text_start", contentIndex, partial };
|
|
443
|
+
yield { type: "text_delta", contentIndex, delta: accepted.text, partial };
|
|
444
|
+
yield {
|
|
445
|
+
type: "text_end",
|
|
446
|
+
contentIndex,
|
|
447
|
+
content: accepted.text,
|
|
448
|
+
partial,
|
|
449
|
+
...(accepted.textSignature === undefined
|
|
450
|
+
? {}
|
|
451
|
+
: { contentSignature: accepted.textSignature }),
|
|
452
|
+
} as AssistantMessageEvent;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (accepted.type === "thinking") {
|
|
456
|
+
yield { type: "thinking_start", contentIndex, partial };
|
|
457
|
+
yield { type: "thinking_delta", contentIndex, delta: accepted.thinking, partial };
|
|
458
|
+
yield {
|
|
459
|
+
type: "thinking_end",
|
|
460
|
+
contentIndex,
|
|
461
|
+
content: accepted.thinking,
|
|
462
|
+
partial,
|
|
463
|
+
...(accepted.thinkingSignature === undefined
|
|
464
|
+
? {}
|
|
465
|
+
: { contentSignature: accepted.thinkingSignature }),
|
|
466
|
+
...(accepted.redacted === undefined ? {} : { redacted: accepted.redacted }),
|
|
467
|
+
} as AssistantMessageEvent;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
yield { type: "toolcall_start", contentIndex, partial };
|
|
471
|
+
yield {
|
|
472
|
+
type: "toolcall_delta",
|
|
473
|
+
contentIndex,
|
|
474
|
+
delta: jsonText(accepted.arguments),
|
|
475
|
+
partial,
|
|
476
|
+
};
|
|
477
|
+
yield { type: "toolcall_end", contentIndex, toolCall: accepted, partial };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
yield {
|
|
481
|
+
type: "done",
|
|
482
|
+
reason: terminal.stopReason as Extract<AssistantMessage["stopReason"], "stop" | "length" | "toolUse" | "deferred">,
|
|
483
|
+
message: terminal,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Consume one candidate physical attempt privately and publish only an accepted
|
|
489
|
+
* successful terminal. This function never selects or retries an account.
|
|
490
|
+
*/
|
|
491
|
+
export function createAcceptedOutputStream(
|
|
492
|
+
upstream: AsyncIterable<unknown> | Promise<AsyncIterable<unknown>>,
|
|
493
|
+
options: AcceptedOutputOptions = {},
|
|
494
|
+
): AssistantMessageEventStream {
|
|
495
|
+
const limits = assertLimits(options);
|
|
496
|
+
let state: AttemptState = { status: "pending" };
|
|
497
|
+
let claimed = false;
|
|
498
|
+
let iterator: AsyncIterator<unknown> | undefined;
|
|
499
|
+
let sourceReturn: Promise<void> | undefined;
|
|
500
|
+
let resolveResult!: (message: AssistantMessage) => void;
|
|
501
|
+
let rejectResult!: (error: AcceptedOutputError) => void;
|
|
502
|
+
const resultPromise = new Promise<AssistantMessage>((resolve, reject) => {
|
|
503
|
+
resolveResult = resolve;
|
|
504
|
+
rejectResult = reject;
|
|
505
|
+
});
|
|
506
|
+
// A caller may consume iteration only. Mark the independent result branch as
|
|
507
|
+
// observed so a rejected candidate cannot become an unhandled rejection.
|
|
508
|
+
void resultPromise.catch(() => {});
|
|
509
|
+
// Observe the caller-supplied promise synchronously. A pre-aborted signal must
|
|
510
|
+
// not leave an already-rejecting upstream without a rejection handler.
|
|
511
|
+
const upstreamPromise: Promise<AsyncIterable<unknown>> = Promise.resolve(upstream);
|
|
512
|
+
void upstreamPromise.catch(() => {});
|
|
513
|
+
|
|
514
|
+
const fail = (error: AcceptedOutputError): void => {
|
|
515
|
+
if (state.status !== "pending") return;
|
|
516
|
+
state = { status: "failed", error };
|
|
517
|
+
rejectResult(error);
|
|
518
|
+
};
|
|
519
|
+
const accept = (terminal: AssistantMessage, partial: AssistantMessage): void => {
|
|
520
|
+
if (state.status !== "pending") return;
|
|
521
|
+
state = { status: "accepted", terminal, partial };
|
|
522
|
+
resolveResult(terminal);
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
type AbortRace<T> =
|
|
526
|
+
| { readonly kind: "value"; readonly value: T }
|
|
527
|
+
| { readonly kind: "rejected"; readonly error: unknown }
|
|
528
|
+
| { readonly kind: "aborted" };
|
|
529
|
+
type TerminalCleanupRace = AbortRace<void> | { readonly kind: "timed-out" };
|
|
530
|
+
|
|
531
|
+
let abortRequested = false;
|
|
532
|
+
let abortWaiter: (() => void) | undefined;
|
|
533
|
+
const onAbort = (): void => {
|
|
534
|
+
if (abortRequested) return;
|
|
535
|
+
abortRequested = true;
|
|
536
|
+
const wake = abortWaiter;
|
|
537
|
+
abortWaiter = undefined;
|
|
538
|
+
wake?.();
|
|
539
|
+
};
|
|
540
|
+
if (options.signal?.aborted) onAbort();
|
|
541
|
+
else options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
542
|
+
|
|
543
|
+
// A fresh, short-lived abort promise keeps at most one waiter reachable. In
|
|
544
|
+
// contrast, repeatedly chaining from one never-settled promise retains one
|
|
545
|
+
// reaction closure for every producer event until cancellation.
|
|
546
|
+
const raceWithAbort = async <T>(operation: Promise<T>): Promise<AbortRace<T>> => {
|
|
547
|
+
if (abortRequested) return { kind: "aborted" };
|
|
548
|
+
let wake!: () => void;
|
|
549
|
+
const abortForOperation = new Promise<AbortRace<T>>((resolve) => {
|
|
550
|
+
wake = () => resolve({ kind: "aborted" });
|
|
551
|
+
abortWaiter = wake;
|
|
552
|
+
});
|
|
553
|
+
if (abortRequested) wake();
|
|
554
|
+
try {
|
|
555
|
+
return await Promise.race([
|
|
556
|
+
operation.then(
|
|
557
|
+
(value): AbortRace<T> => ({ kind: "value", value }),
|
|
558
|
+
(error: unknown): AbortRace<T> => ({ kind: "rejected", error }),
|
|
559
|
+
),
|
|
560
|
+
abortForOperation,
|
|
561
|
+
]);
|
|
562
|
+
} finally {
|
|
563
|
+
if (abortWaiter === wake) abortWaiter = undefined;
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const raceTerminalCleanup = async (
|
|
568
|
+
operation: Promise<void>,
|
|
569
|
+
): Promise<TerminalCleanupRace> => {
|
|
570
|
+
if (abortRequested) return { kind: "aborted" };
|
|
571
|
+
let wake!: () => void;
|
|
572
|
+
let cleanupTimer: ReturnType<typeof setTimeout> | undefined;
|
|
573
|
+
const abortForOperation = new Promise<TerminalCleanupRace>((resolve) => {
|
|
574
|
+
wake = () => resolve({ kind: "aborted" });
|
|
575
|
+
abortWaiter = wake;
|
|
576
|
+
});
|
|
577
|
+
const cleanupDeadline = new Promise<TerminalCleanupRace>((resolve) => {
|
|
578
|
+
cleanupTimer = setTimeout(
|
|
579
|
+
() => resolve({ kind: "timed-out" }),
|
|
580
|
+
TERMINAL_CLEANUP_TIMEOUT_MS,
|
|
581
|
+
);
|
|
582
|
+
});
|
|
583
|
+
if (abortRequested) wake();
|
|
584
|
+
try {
|
|
585
|
+
return await Promise.race([
|
|
586
|
+
operation.then(
|
|
587
|
+
(): TerminalCleanupRace => ({ kind: "value", value: undefined }),
|
|
588
|
+
(error: unknown): TerminalCleanupRace => ({ kind: "rejected", error }),
|
|
589
|
+
),
|
|
590
|
+
abortForOperation,
|
|
591
|
+
cleanupDeadline,
|
|
592
|
+
]);
|
|
593
|
+
} finally {
|
|
594
|
+
if (cleanupTimer !== undefined) clearTimeout(cleanupTimer);
|
|
595
|
+
if (abortWaiter === wake) abortWaiter = undefined;
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const releaseSource = (): Promise<void> => {
|
|
600
|
+
if (sourceReturn !== undefined) return sourceReturn;
|
|
601
|
+
const active = iterator;
|
|
602
|
+
if (active === undefined) return Promise.resolve();
|
|
603
|
+
iterator = undefined;
|
|
604
|
+
try {
|
|
605
|
+
const close = active.return;
|
|
606
|
+
sourceReturn = close === undefined
|
|
607
|
+
? Promise.resolve()
|
|
608
|
+
: Promise.resolve(close.call(active)).then(() => undefined);
|
|
609
|
+
} catch (error) {
|
|
610
|
+
sourceReturn = Promise.reject(error);
|
|
611
|
+
}
|
|
612
|
+
void sourceReturn.catch(() => {});
|
|
613
|
+
return sourceReturn;
|
|
614
|
+
};
|
|
615
|
+
const releaseSourceQuietly = (): void => {
|
|
616
|
+
void releaseSource().catch(() => {});
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// Opening is started even for a pre-aborted call. If a promised source arrives
|
|
620
|
+
// after cancellation, acquiring and immediately returning its iterator gives
|
|
621
|
+
// the producer one deterministic cleanup path instead of abandoning it.
|
|
622
|
+
const openedPromise = upstreamPromise.then((value) => {
|
|
623
|
+
if (!isRecord(value) || !(Symbol.asyncIterator in value)) {
|
|
624
|
+
throw new AcceptedOutputError("malformed");
|
|
625
|
+
}
|
|
626
|
+
let opened: AsyncIterator<unknown>;
|
|
627
|
+
try {
|
|
628
|
+
opened = value[Symbol.asyncIterator]();
|
|
629
|
+
} catch {
|
|
630
|
+
throw new AcceptedOutputError("malformed");
|
|
631
|
+
}
|
|
632
|
+
iterator = opened;
|
|
633
|
+
if (abortRequested) releaseSourceQuietly();
|
|
634
|
+
return opened;
|
|
635
|
+
});
|
|
636
|
+
void openedPromise.catch(() => {});
|
|
637
|
+
|
|
638
|
+
void (async () => {
|
|
639
|
+
let eventCount = 0;
|
|
640
|
+
let byteCount = 0;
|
|
641
|
+
try {
|
|
642
|
+
if (abortRequested) {
|
|
643
|
+
fail(new AcceptedOutputError("aborted"));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const opened = await raceWithAbort(openedPromise);
|
|
647
|
+
if (opened.kind === "aborted" || abortRequested) {
|
|
648
|
+
fail(new AcceptedOutputError("aborted"));
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (opened.kind === "rejected") {
|
|
652
|
+
fail(
|
|
653
|
+
opened.error instanceof AcceptedOutputError
|
|
654
|
+
? opened.error
|
|
655
|
+
: new AcceptedOutputError("rejected"),
|
|
656
|
+
);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
while (state.status === "pending") {
|
|
661
|
+
if (abortRequested) {
|
|
662
|
+
releaseSourceQuietly();
|
|
663
|
+
fail(new AcceptedOutputError("aborted"));
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
let nextPromise: Promise<IteratorResult<unknown>>;
|
|
667
|
+
try {
|
|
668
|
+
nextPromise = Promise.resolve(opened.value.next());
|
|
669
|
+
} catch {
|
|
670
|
+
releaseSourceQuietly();
|
|
671
|
+
fail(new AcceptedOutputError("rejected"));
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const next = await raceWithAbort(nextPromise);
|
|
675
|
+
if (next.kind === "aborted" || abortRequested) {
|
|
676
|
+
releaseSourceQuietly();
|
|
677
|
+
fail(new AcceptedOutputError("aborted"));
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (next.kind === "rejected") {
|
|
681
|
+
releaseSourceQuietly();
|
|
682
|
+
fail(new AcceptedOutputError("rejected"));
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (next.value.done) {
|
|
686
|
+
fail(new AcceptedOutputError("malformed"));
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (!isAssistantMessageEventEnvelope(next.value.value)) {
|
|
690
|
+
releaseSourceQuietly();
|
|
691
|
+
fail(new AcceptedOutputError("malformed"));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const event = next.value.value;
|
|
696
|
+
eventCount += 1;
|
|
697
|
+
if (eventCount > limits.maxEvents) {
|
|
698
|
+
releaseSourceQuietly();
|
|
699
|
+
fail(new AcceptedOutputError("overflow"));
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
byteCount += deltaBytes(event);
|
|
703
|
+
if (byteCount > limits.maxBufferedBytes) {
|
|
704
|
+
releaseSourceQuietly();
|
|
705
|
+
fail(new AcceptedOutputError("overflow"));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (event.type === "error") {
|
|
710
|
+
releaseSourceQuietly();
|
|
711
|
+
if (!isAssistantMessage(event.error)) {
|
|
712
|
+
fail(new AcceptedOutputError("malformed"));
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
fail(new AcceptedOutputError(event.reason === "aborted" ? "aborted" : "rejected"));
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (event.type === "done") {
|
|
719
|
+
const released = await raceTerminalCleanup(releaseSource());
|
|
720
|
+
if (released.kind === "aborted" || abortRequested) {
|
|
721
|
+
fail(new AcceptedOutputError("aborted"));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (released.kind === "rejected" || released.kind === "timed-out") {
|
|
725
|
+
fail(new AcceptedOutputError("rejected"));
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
// Cleanup is allowed to run producer finally blocks. Revalidate and
|
|
729
|
+
// deeply validate the authoritative terminal only now, before sizing
|
|
730
|
+
// or cloning, so replay and exact result cannot diverge.
|
|
731
|
+
if (
|
|
732
|
+
!isAssistantMessageEventEnvelope(event) ||
|
|
733
|
+
event.type !== "done" ||
|
|
734
|
+
!isAssistantMessage(event.message)
|
|
735
|
+
) {
|
|
736
|
+
fail(new AcceptedOutputError("malformed"));
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const replayEvents = 2 + event.message.content.length * 3;
|
|
740
|
+
if (replayEvents > limits.maxEvents) {
|
|
741
|
+
fail(new AcceptedOutputError("overflow"));
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
let projectedBytes: number;
|
|
745
|
+
try {
|
|
746
|
+
projectedBytes = projectedAcceptedBytes(
|
|
747
|
+
event.message,
|
|
748
|
+
limits.maxBufferedBytes,
|
|
749
|
+
);
|
|
750
|
+
} catch {
|
|
751
|
+
fail(new AcceptedOutputError("malformed"));
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (projectedBytes > limits.maxBufferedBytes) {
|
|
755
|
+
fail(new AcceptedOutputError("overflow"));
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
let partial: AssistantMessage;
|
|
759
|
+
try {
|
|
760
|
+
partial = acceptedPartial(event.message);
|
|
761
|
+
} catch {
|
|
762
|
+
fail(new AcceptedOutputError("malformed"));
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
accept(event.message, partial);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
} catch (error) {
|
|
770
|
+
releaseSourceQuietly();
|
|
771
|
+
fail(error instanceof AcceptedOutputError ? error : new AcceptedOutputError("malformed"));
|
|
772
|
+
} finally {
|
|
773
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
774
|
+
iterator = undefined;
|
|
775
|
+
}
|
|
776
|
+
})();
|
|
777
|
+
|
|
778
|
+
const stream = {
|
|
779
|
+
[Symbol.asyncIterator](): AsyncIterator<AssistantMessageEvent> {
|
|
780
|
+
if (claimed) {
|
|
781
|
+
return {
|
|
782
|
+
next: () => Promise.reject(new AcceptedOutputError("malformed")),
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
claimed = true;
|
|
786
|
+
let closed = false;
|
|
787
|
+
let replay: AsyncIterator<AssistantMessageEvent> | undefined;
|
|
788
|
+
return {
|
|
789
|
+
async next(): Promise<IteratorResult<AssistantMessageEvent>> {
|
|
790
|
+
if (closed) return { value: undefined, done: true };
|
|
791
|
+
if (replay === undefined) {
|
|
792
|
+
await resultPromise;
|
|
793
|
+
if (closed) return { value: undefined, done: true };
|
|
794
|
+
if (state.status !== "accepted" || state.partial === undefined) {
|
|
795
|
+
throw new AcceptedOutputError("malformed");
|
|
796
|
+
}
|
|
797
|
+
const { terminal, partial } = state;
|
|
798
|
+
replay = acceptedEvents(terminal, partial)[Symbol.asyncIterator]();
|
|
799
|
+
// The replay iterator now owns the sole accepted snapshot. The stream
|
|
800
|
+
// retains only the exact terminal object required by result().
|
|
801
|
+
state = { status: "accepted", terminal, partial: undefined };
|
|
802
|
+
}
|
|
803
|
+
const next = await replay.next();
|
|
804
|
+
if (closed) return { value: undefined, done: true };
|
|
805
|
+
if (next.done) {
|
|
806
|
+
replay = undefined;
|
|
807
|
+
closed = true;
|
|
808
|
+
}
|
|
809
|
+
return next;
|
|
810
|
+
},
|
|
811
|
+
async return(): Promise<IteratorResult<AssistantMessageEvent>> {
|
|
812
|
+
if (closed) return { value: undefined, done: true };
|
|
813
|
+
closed = true;
|
|
814
|
+
onAbort();
|
|
815
|
+
releaseSourceQuietly();
|
|
816
|
+
if (state.status === "accepted" && state.partial !== undefined) {
|
|
817
|
+
state = { status: "accepted", terminal: state.terminal, partial: undefined };
|
|
818
|
+
}
|
|
819
|
+
if (replay?.return !== undefined) {
|
|
820
|
+
try {
|
|
821
|
+
await replay.return();
|
|
822
|
+
} catch {
|
|
823
|
+
// The replay is local and content-free on cleanup failure.
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
replay = undefined;
|
|
827
|
+
return { value: undefined, done: true };
|
|
828
|
+
},
|
|
829
|
+
};
|
|
830
|
+
},
|
|
831
|
+
result(): Promise<AssistantMessage> {
|
|
832
|
+
return resultPromise;
|
|
833
|
+
},
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
return stream as AssistantMessageEventStream;
|
|
837
|
+
}
|