cursor-opencode-provider 0.8.0 → 0.8.3
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/CHANGELOG.md +174 -0
- package/README.md +230 -44
- package/SECURITY.md +60 -12
- package/dist/activity.d.ts +73 -1
- package/dist/activity.js +231 -5
- package/dist/agent-url.js +37 -9
- package/dist/auth-renewal.d.ts +1 -1
- package/dist/auth-renewal.js +58 -26
- package/dist/billing.d.ts +105 -0
- package/dist/billing.js +212 -0
- package/dist/classic-tool-results.d.ts +1 -0
- package/dist/classic-tool-results.js +42 -0
- package/dist/context/build.d.ts +23 -4
- package/dist/context/build.js +65 -10
- package/dist/context/dynamic-catalog.js +6 -1
- package/dist/context/env.js +27 -3
- package/dist/context/epoch.js +9 -5
- package/dist/context/frozen.js +6 -2
- package/dist/context/git.d.ts +5 -2
- package/dist/context/git.js +17 -7
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/context/paths.d.ts +45 -18
- package/dist/context/paths.js +72 -22
- package/dist/context/skills-bridge.d.ts +22 -0
- package/dist/context/skills-bridge.js +20 -0
- package/dist/context/skills.d.ts +34 -0
- package/dist/context/skills.js +211 -0
- package/dist/debug.js +13 -2
- package/dist/early-tool-rows.d.ts +60 -0
- package/dist/early-tool-rows.js +96 -0
- package/dist/errors.d.ts +49 -7
- package/dist/errors.js +163 -8
- package/dist/host-agent-mode.d.ts +53 -4
- package/dist/host-agent-mode.js +117 -13
- package/dist/host-interrupt.d.ts +12 -0
- package/dist/host-interrupt.js +18 -0
- package/dist/host-plan-file.d.ts +28 -0
- package/dist/host-plan-file.js +76 -0
- package/dist/host-steer.d.ts +71 -0
- package/dist/host-steer.js +232 -0
- package/dist/image-input.d.ts +18 -0
- package/dist/image-input.js +122 -30
- package/dist/image-save.d.ts +10 -2
- package/dist/image-save.js +33 -21
- package/dist/index.d.ts +2 -2
- package/dist/language-model.d.ts +149 -30
- package/dist/language-model.js +3127 -673
- package/dist/lost-request.d.ts +39 -0
- package/dist/lost-request.js +111 -0
- package/dist/model-config.d.ts +12 -1
- package/dist/model-config.js +78 -30
- package/dist/models.d.ts +26 -3
- package/dist/models.js +101 -45
- package/dist/opencode2/catalog.d.ts +1 -0
- package/dist/opencode2/catalog.js +2 -0
- package/dist/opencode2/image-save-tool.d.ts +8 -0
- package/dist/opencode2/image-save-tool.js +84 -0
- package/dist/opencode2/tool-results.d.ts +18 -0
- package/dist/opencode2/tool-results.js +66 -0
- package/dist/opencode2/types.d.ts +15 -0
- package/dist/parallel-step.d.ts +65 -0
- package/dist/parallel-step.js +102 -0
- package/dist/plugin-opencode2.js +233 -65
- package/dist/plugin.js +97 -23
- package/dist/pricing-data.d.ts +40 -3
- package/dist/pricing-data.js +40 -3
- package/dist/pricing.d.ts +1 -1
- package/dist/pricing.js +7 -4
- package/dist/process-shared.d.ts +14 -0
- package/dist/process-shared.js +18 -0
- package/dist/protocol/apply-patch.d.ts +25 -13
- package/dist/protocol/apply-patch.js +79 -30
- package/dist/protocol/ask-question.d.ts +12 -1
- package/dist/protocol/ask-question.js +77 -3
- package/dist/protocol/blob-store.d.ts +5 -1
- package/dist/protocol/blob-store.js +10 -6
- package/dist/protocol/client-version.js +6 -4
- package/dist/protocol/conversation-cache-header.d.ts +23 -0
- package/dist/protocol/conversation-cache-header.js +149 -0
- package/dist/protocol/conversation-persistence.d.ts +27 -1
- package/dist/protocol/conversation-persistence.js +628 -126
- package/dist/protocol/conversation-state.d.ts +8 -1
- package/dist/protocol/conversation-state.js +28 -7
- package/dist/protocol/create-plan.d.ts +47 -25
- package/dist/protocol/create-plan.js +33 -23
- package/dist/protocol/host-notes.d.ts +31 -0
- package/dist/protocol/host-notes.js +85 -0
- package/dist/protocol/interactions.d.ts +26 -17
- package/dist/protocol/interactions.js +80 -31
- package/dist/protocol/messages.js +164 -21
- package/dist/protocol/request.d.ts +40 -19
- package/dist/protocol/request.js +55 -28
- package/dist/protocol/sent-history-images.d.ts +49 -0
- package/dist/protocol/sent-history-images.js +102 -0
- package/dist/protocol/switch-mode.d.ts +66 -27
- package/dist/protocol/switch-mode.js +147 -73
- package/dist/protocol/token-details.d.ts +2 -0
- package/dist/protocol/token-details.js +11 -1
- package/dist/protocol/tool-call-bridge.d.ts +6 -0
- package/dist/protocol/tool-call-bridge.js +24 -0
- package/dist/protocol/tools.d.ts +90 -9
- package/dist/protocol/tools.js +543 -151
- package/dist/protocol/turn-provenance.d.ts +19 -4
- package/dist/protocol/turn-provenance.js +77 -20
- package/dist/replay-safety.d.ts +22 -4
- package/dist/replay-safety.js +86 -27
- package/dist/retry-budget.d.ts +21 -0
- package/dist/retry-budget.js +94 -0
- package/dist/runtime-files.d.ts +23 -0
- package/dist/runtime-files.js +581 -0
- package/dist/session.d.ts +129 -10
- package/dist/session.js +198 -35
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +1 -1
- package/dist/shell-timeout.d.ts +54 -10
- package/dist/shell-timeout.js +362 -92
- package/dist/stream-write.d.ts +20 -0
- package/dist/stream-write.js +87 -0
- package/dist/terminal-runner.d.ts +15 -0
- package/dist/terminal-runner.js +528 -0
- package/dist/todo-store.d.ts +2 -0
- package/dist/todo-store.js +19 -0
- package/dist/transport/connect.d.ts +10 -1
- package/dist/transport/connect.js +124 -27
- package/dist/usage.d.ts +37 -17
- package/dist/usage.js +85 -34
- package/package.json +5 -3
- package/dist/plan-execution-kickoff.d.ts +0 -57
- package/dist/plan-execution-kickoff.js +0 -186
- package/dist/protocol/thinking.d.ts +0 -15
- package/dist/protocol/thinking.js +0 -17
package/dist/todo-store.js
CHANGED
|
@@ -54,6 +54,25 @@ export function normalizeSessionTodos(value) {
|
|
|
54
54
|
}
|
|
55
55
|
return out;
|
|
56
56
|
}
|
|
57
|
+
/** Fill omitted defaults before validation without dropping malformed entries. */
|
|
58
|
+
export function normalizeOpencodeTodoArgs(args) {
|
|
59
|
+
if (!Array.isArray(args.todos))
|
|
60
|
+
return args;
|
|
61
|
+
return {
|
|
62
|
+
...args,
|
|
63
|
+
todos: args.todos.map((item, index) => {
|
|
64
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
65
|
+
return item;
|
|
66
|
+
const todo = item;
|
|
67
|
+
return {
|
|
68
|
+
...todo,
|
|
69
|
+
...(todo.id === undefined ? { id: String(index + 1) } : {}),
|
|
70
|
+
...(todo.status === undefined ? { status: "pending" } : {}),
|
|
71
|
+
...(todo.priority === undefined ? { priority: "medium" } : {}),
|
|
72
|
+
};
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
57
76
|
export function getSessionTodos(sessionID) {
|
|
58
77
|
return lists.get(sessionID)?.map((todo) => ({ ...todo })) ?? [];
|
|
59
78
|
}
|
|
@@ -35,7 +35,12 @@ export declare function fetchAgentUrl(token: string, options?: {
|
|
|
35
35
|
}): Promise<string>;
|
|
36
36
|
export type BidiStream = {
|
|
37
37
|
write(msg: Uint8Array): boolean | void;
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Resolves once a backpressured write has drained. There is no deadline, as
|
|
40
|
+
* in Cursor CLI's transport: a stream error or close (remote or local) settles
|
|
41
|
+
* it, and the read-idle watchdog bounds a stream that stops answering.
|
|
42
|
+
*/
|
|
43
|
+
waitForDrain?(): Promise<void>;
|
|
39
44
|
end(): void;
|
|
40
45
|
frames(): AsyncIterable<{
|
|
41
46
|
flags: number;
|
|
@@ -73,9 +78,13 @@ export declare function resolveAgentOrigin(baseURL: string): string;
|
|
|
73
78
|
export declare function closeCachedHttp2SessionsForTests(): void;
|
|
74
79
|
/** Node-runtime regression hook; production callers use getSession(). */
|
|
75
80
|
export declare function installSessionInvalidationForTests(origin: string, session: http2.ClientHttp2Session): void;
|
|
81
|
+
/** Node-runtime regression hook: cache a connected local session for `origin`. */
|
|
82
|
+
export declare function cacheHttp2SessionForTests(origin: string, session: http2.ClientHttp2Session): void;
|
|
76
83
|
export declare function getSession(baseURL: string, options?: {
|
|
77
84
|
pingTimeoutMs?: number;
|
|
78
85
|
}): Promise<http2.ClientHttp2Session>;
|
|
86
|
+
export declare function frameArrival(frame: object): number | undefined;
|
|
87
|
+
export declare function lastFrameArrival(): number;
|
|
79
88
|
export declare function bidiRunStream(token: string, options: {
|
|
80
89
|
signal?: AbortSignal;
|
|
81
90
|
baseURL: string;
|
|
@@ -194,6 +194,7 @@ export class CursorRunInterruptedError extends CursorTransportError {
|
|
|
194
194
|
transient: true,
|
|
195
195
|
replaySafe: true,
|
|
196
196
|
cause: options?.cause,
|
|
197
|
+
code: options?.cause === undefined ? "CURSOR_STREAM_CLOSED_EARLY" : undefined,
|
|
197
198
|
});
|
|
198
199
|
this.name = "CursorRunInterruptedError";
|
|
199
200
|
}
|
|
@@ -234,6 +235,12 @@ function cursorRunGrpcError(grpcStatus, headers, trailers) {
|
|
|
234
235
|
const _http2Sessions = new Map();
|
|
235
236
|
const _http2SessionCreatedAt = new WeakMap();
|
|
236
237
|
const _http2SessionListenerCleanup = new WeakMap();
|
|
238
|
+
// As in Cursor CLI's transport (@connectrpc/connect-node 1.6.1
|
|
239
|
+
// Http2SessionManager), a cached session keeps the process alive only while a
|
|
240
|
+
// Run stream or a reuse ping is live on it: unref'd once connected, ref'd while
|
|
241
|
+
// it holds one. A Run held open on a pending exec keeps its stream, so it stays
|
|
242
|
+
// ref'd; an idle session would otherwise stop a headless host from exiting.
|
|
243
|
+
const _http2SessionHolds = new WeakMap();
|
|
237
244
|
// Validation and connect work for the same origin shares one Promise.
|
|
238
245
|
const _http2Connecting = new Map();
|
|
239
246
|
// Bound the time we'll wait for the initial connect. Without this, a dead or
|
|
@@ -242,8 +249,16 @@ const _http2Connecting = new Map();
|
|
|
242
249
|
const CONNECT_TIMEOUT_MS = 15_000;
|
|
243
250
|
const DEFAULT_SESSION_PING_TIMEOUT_MS = 5_000;
|
|
244
251
|
const DEFAULT_READ_IDLE_MS = 120_000;
|
|
245
|
-
const WRITE_DRAIN_TIMEOUT_CODE = "CURSOR_WRITE_DRAIN_TIMEOUT";
|
|
246
252
|
export const HTTP2_SESSION_MAX_AGE_MS = 15 * 60_000;
|
|
253
|
+
// Every Run shares one session per origin. Node and Bun count its unsent bytes
|
|
254
|
+
// against maxSessionMemory (10 MB by default) and, once over it, fail every new
|
|
255
|
+
// stream with ENHANCE_YOUR_CALM (Bun before sending it, Node by refusing its
|
|
256
|
+
// response headers); Node also fails a single write over it while the uplink
|
|
257
|
+
// holds the bytes back. Run requests and KV replies run to many megabytes, and
|
|
258
|
+
// the provider holds those bytes anyway, so the limit only adds a failure.
|
|
259
|
+
// 256 MB clears the 100 MiB checkpoint transfer budget with room to spare.
|
|
260
|
+
// Cursor CLI's transport sets no maxSessionMemory, so it keeps the default (#76).
|
|
261
|
+
const RUN_SESSION_OPTIONS = { maxSessionMemory: 256 };
|
|
247
262
|
export function shouldReuseHttp2Session(state, createdAt, now = Date.now()) {
|
|
248
263
|
return !state.destroyed && !state.closed && now - createdAt < HTTP2_SESSION_MAX_AGE_MS;
|
|
249
264
|
}
|
|
@@ -259,6 +274,42 @@ function dropSession(origin, session) {
|
|
|
259
274
|
if (_http2Sessions.get(origin) === session)
|
|
260
275
|
_http2Sessions.delete(origin);
|
|
261
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Stop offering `session` for new Runs and close it so in-flight streams can
|
|
279
|
+
* drain. Used by age rotation and a failed reuse ping — never
|
|
280
|
+
* `destroy()`, which would abort every sibling Run multiplexed on the session
|
|
281
|
+
* mid-output (those Runs cannot be replayed after visible activity).
|
|
282
|
+
*/
|
|
283
|
+
function retireCachedSession(origin, session) {
|
|
284
|
+
dropSession(origin, session);
|
|
285
|
+
try {
|
|
286
|
+
session.close();
|
|
287
|
+
}
|
|
288
|
+
catch { /* already closed */ }
|
|
289
|
+
}
|
|
290
|
+
function holdHttp2Session(session) {
|
|
291
|
+
const holds = _http2SessionHolds.get(session) ?? 0;
|
|
292
|
+
_http2SessionHolds.set(session, holds + 1);
|
|
293
|
+
if (holds === 0)
|
|
294
|
+
session.ref();
|
|
295
|
+
}
|
|
296
|
+
function releaseHttp2Session(session) {
|
|
297
|
+
const holds = (_http2SessionHolds.get(session) ?? 0) - 1;
|
|
298
|
+
if (holds > 0) {
|
|
299
|
+
_http2SessionHolds.set(session, holds);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
_http2SessionHolds.delete(session);
|
|
303
|
+
if (!session.destroyed)
|
|
304
|
+
session.unref();
|
|
305
|
+
}
|
|
306
|
+
/** Offer a connected session for new Runs; it stays unref'd until one holds it. */
|
|
307
|
+
function cacheReadySession(origin, session) {
|
|
308
|
+
if (!_http2SessionHolds.has(session))
|
|
309
|
+
session.unref();
|
|
310
|
+
_http2SessionCreatedAt.set(session, Date.now());
|
|
311
|
+
_http2Sessions.set(origin, session);
|
|
312
|
+
}
|
|
262
313
|
/** Test cleanup for local HTTP/2 fixtures; production sessions stay process-cached. */
|
|
263
314
|
export function closeCachedHttp2SessionsForTests() {
|
|
264
315
|
for (const session of _http2Sessions.values()) {
|
|
@@ -354,6 +405,13 @@ function installSessionInvalidation(origin, session) {
|
|
|
354
405
|
export function installSessionInvalidationForTests(origin, session) {
|
|
355
406
|
installSessionInvalidation(origin, session);
|
|
356
407
|
}
|
|
408
|
+
/** Node-runtime regression hook: cache a connected local session for `origin`. */
|
|
409
|
+
export function cacheHttp2SessionForTests(origin, session) {
|
|
410
|
+
// Tests re-cache one client per turn; replace its listeners rather than stack them.
|
|
411
|
+
_http2SessionListenerCleanup.get(session)?.();
|
|
412
|
+
installSessionInvalidation(origin, session);
|
|
413
|
+
cacheReadySession(origin, session);
|
|
414
|
+
}
|
|
357
415
|
function invalidateSession(origin, session) {
|
|
358
416
|
dropSession(origin, session);
|
|
359
417
|
_http2SessionListenerCleanup.get(session)?.();
|
|
@@ -467,8 +525,7 @@ function connectSession(origin) {
|
|
|
467
525
|
}
|
|
468
526
|
// The TLS socket (if any) is owned by the HTTP/2 session from here on.
|
|
469
527
|
tunnelSocket = undefined;
|
|
470
|
-
|
|
471
|
-
_http2Sessions.set(origin, session);
|
|
528
|
+
cacheReadySession(origin, session);
|
|
472
529
|
trace(`h2 session connected: origin=${origin}`);
|
|
473
530
|
resolve(session);
|
|
474
531
|
};
|
|
@@ -499,11 +556,12 @@ function connectSession(origin) {
|
|
|
499
556
|
}
|
|
500
557
|
tunnelSocket = tlsSocket;
|
|
501
558
|
session = http2.connect(origin, {
|
|
559
|
+
...RUN_SESSION_OPTIONS,
|
|
502
560
|
createConnection: () => tlsSocket,
|
|
503
561
|
});
|
|
504
562
|
}
|
|
505
563
|
else {
|
|
506
|
-
session = http2.connect(origin);
|
|
564
|
+
session = http2.connect(origin, RUN_SESSION_OPTIONS);
|
|
507
565
|
}
|
|
508
566
|
if (settled) {
|
|
509
567
|
try {
|
|
@@ -535,23 +593,27 @@ export function getSession(baseURL, options = {}) {
|
|
|
535
593
|
const createdAt = _http2SessionCreatedAt.get(existing) ?? 0;
|
|
536
594
|
if (!shouldReuseHttp2Session(existing, createdAt)) {
|
|
537
595
|
trace(`h2 session rotate: origin=${origin} ageMs=${Math.max(0, Date.now() - createdAt)}`);
|
|
538
|
-
|
|
539
|
-
try {
|
|
540
|
-
existing.close();
|
|
541
|
-
}
|
|
542
|
-
catch { /* already closed */ }
|
|
596
|
+
retireCachedSession(origin, existing);
|
|
543
597
|
}
|
|
544
598
|
else {
|
|
545
599
|
try {
|
|
546
|
-
|
|
600
|
+
holdHttp2Session(existing);
|
|
601
|
+
try {
|
|
602
|
+
await validateCachedSession(existing, pingTimeoutMs);
|
|
603
|
+
}
|
|
604
|
+
finally {
|
|
605
|
+
releaseHttp2Session(existing);
|
|
606
|
+
}
|
|
547
607
|
if (_http2Sessions.get(origin) === existing && !existing.destroyed && !existing.closed) {
|
|
548
608
|
trace(`h2 cached session ping ok: origin=${origin}`);
|
|
549
609
|
return existing;
|
|
550
610
|
}
|
|
551
611
|
}
|
|
552
612
|
catch (error) {
|
|
613
|
+
// Sibling Runs may still be streaming on this session; destroy() would
|
|
614
|
+
// abort them mid-output, where they cannot be replayed.
|
|
553
615
|
trace(`h2 cached session ping failed: origin=${origin} err=${error.message}`);
|
|
554
|
-
|
|
616
|
+
retireCachedSession(origin, existing);
|
|
555
617
|
}
|
|
556
618
|
}
|
|
557
619
|
}
|
|
@@ -565,6 +627,15 @@ export function getSession(baseURL, options = {}) {
|
|
|
565
627
|
promise.then(cleanup, cleanup);
|
|
566
628
|
return promise;
|
|
567
629
|
}
|
|
630
|
+
/** Process-wide order in which Run frames came off the network, before any reader takes them. */
|
|
631
|
+
const frameArrivals = new WeakMap();
|
|
632
|
+
let lastArrival = 0;
|
|
633
|
+
export function frameArrival(frame) {
|
|
634
|
+
return frameArrivals.get(frame);
|
|
635
|
+
}
|
|
636
|
+
export function lastFrameArrival() {
|
|
637
|
+
return lastArrival;
|
|
638
|
+
}
|
|
568
639
|
export async function bidiRunStream(token, options) {
|
|
569
640
|
const origin = resolveAgentOrigin(options.baseURL);
|
|
570
641
|
const readIdleMs = timeoutMs("Cursor provider readIdleMs", options.readIdleMs, DEFAULT_READ_IDLE_MS);
|
|
@@ -587,6 +658,7 @@ export async function bidiRunStream(token, options) {
|
|
|
587
658
|
const stream = session.request(headers, {
|
|
588
659
|
endStream: false,
|
|
589
660
|
});
|
|
661
|
+
holdHttp2Session(session);
|
|
590
662
|
let writable = true;
|
|
591
663
|
let locallyClosed = false;
|
|
592
664
|
let remotelyClosed = false;
|
|
@@ -626,6 +698,14 @@ export async function bidiRunStream(token, options) {
|
|
|
626
698
|
}
|
|
627
699
|
return undefined;
|
|
628
700
|
};
|
|
701
|
+
const drainTerminalError = (event) => {
|
|
702
|
+
if (event.kind === "local-close") {
|
|
703
|
+
return new CursorLocalCancellationError("Cursor Run stream closed locally during drain");
|
|
704
|
+
}
|
|
705
|
+
if (event.kind === "remote-error")
|
|
706
|
+
return event.error;
|
|
707
|
+
return observedFailure() ?? new CursorRunInterruptedError("Cursor Run stream ended before drain");
|
|
708
|
+
};
|
|
629
709
|
const settleTerminal = (event) => {
|
|
630
710
|
if (terminalEvent)
|
|
631
711
|
return;
|
|
@@ -751,8 +831,10 @@ export async function bidiRunStream(token, options) {
|
|
|
751
831
|
pendingChunks.length = 0;
|
|
752
832
|
if (consumed < merged.length)
|
|
753
833
|
pendingChunks.push(merged.subarray(consumed));
|
|
754
|
-
for (const frame of parsed)
|
|
834
|
+
for (const frame of parsed) {
|
|
835
|
+
frameArrivals.set(frame, ++lastArrival);
|
|
755
836
|
enqueueFrame(frame);
|
|
837
|
+
}
|
|
756
838
|
};
|
|
757
839
|
const startInboundReader = () => {
|
|
758
840
|
if (inboundReaderStarted || locallyClosed || remotelyClosed || streamFailure)
|
|
@@ -795,6 +877,7 @@ export async function bidiRunStream(token, options) {
|
|
|
795
877
|
});
|
|
796
878
|
stream.on("close", () => {
|
|
797
879
|
writable = false;
|
|
880
|
+
releaseHttp2Session(session);
|
|
798
881
|
remotelyClosed = !locallyClosed;
|
|
799
882
|
if (remotelyClosed) {
|
|
800
883
|
dropSession(origin, session);
|
|
@@ -810,6 +893,16 @@ export async function bidiRunStream(token, options) {
|
|
|
810
893
|
`err=${rawStreamError instanceof Error ? rawStreamError.message : "none"})`);
|
|
811
894
|
scheduleTerminalSettlement();
|
|
812
895
|
});
|
|
896
|
+
// Reset with CANCEL, as Cursor CLI's transport does for an aborted call
|
|
897
|
+
// (@connectrpc/connect-node 1.6.1). A plain close() leaves a stream whose
|
|
898
|
+
// request write is still buffered open for good, which also keeps its
|
|
899
|
+
// session held and the process alive.
|
|
900
|
+
const closeLocally = () => {
|
|
901
|
+
try {
|
|
902
|
+
stream.close(http2.constants.NGHTTP2_CANCEL);
|
|
903
|
+
}
|
|
904
|
+
catch { /* already closing */ }
|
|
905
|
+
};
|
|
813
906
|
if (abortSignal) {
|
|
814
907
|
abortHandler = () => {
|
|
815
908
|
if (!locallyClosed) {
|
|
@@ -820,7 +913,7 @@ export async function bidiRunStream(token, options) {
|
|
|
820
913
|
finishInbound();
|
|
821
914
|
stopInboundReader();
|
|
822
915
|
settleTerminal({ kind: "local-close" });
|
|
823
|
-
|
|
916
|
+
closeLocally();
|
|
824
917
|
}
|
|
825
918
|
};
|
|
826
919
|
abortSignal.addEventListener("abort", abortHandler, { once: true });
|
|
@@ -851,20 +944,21 @@ export async function bidiRunStream(token, options) {
|
|
|
851
944
|
throw streamFailure;
|
|
852
945
|
}
|
|
853
946
|
},
|
|
854
|
-
waitForDrain(
|
|
947
|
+
waitForDrain() {
|
|
855
948
|
if (!backpressured)
|
|
856
949
|
return Promise.resolve();
|
|
857
|
-
|
|
950
|
+
if (terminalEvent)
|
|
951
|
+
return Promise.reject(drainTerminalError(terminalEvent));
|
|
858
952
|
return new Promise((resolve, reject) => {
|
|
859
953
|
let settled = false;
|
|
860
954
|
const finish = (error) => {
|
|
861
955
|
if (settled)
|
|
862
956
|
return;
|
|
863
957
|
settled = true;
|
|
864
|
-
clearTimeout(timer);
|
|
865
958
|
stream.removeListener("drain", onDrain);
|
|
866
959
|
stream.removeListener("error", onError);
|
|
867
960
|
stream.removeListener("close", onClose);
|
|
961
|
+
terminalListeners.delete(onTerminal);
|
|
868
962
|
if (error)
|
|
869
963
|
reject(error);
|
|
870
964
|
else
|
|
@@ -884,18 +978,14 @@ export async function bidiRunStream(token, options) {
|
|
|
884
978
|
? new CursorLocalCancellationError("Cursor Run stream closed locally during drain")
|
|
885
979
|
: observedFailure() ?? new CursorRunInterruptedError("Cursor Run stream closed before drain"));
|
|
886
980
|
};
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
stream.destroy(streamFailure);
|
|
892
|
-
}
|
|
893
|
-
catch { /* already closing */ }
|
|
894
|
-
}, drainTimeoutMs);
|
|
895
|
-
timer.unref?.();
|
|
981
|
+
// The Run's end settles it too: Node holds back `close` while a write
|
|
982
|
+
// is buffered, and a response can end while the request is still
|
|
983
|
+
// blocked, after which the read-idle watchdog no longer runs.
|
|
984
|
+
const onTerminal = (event) => finish(drainTerminalError(event));
|
|
896
985
|
stream.once("drain", onDrain);
|
|
897
986
|
stream.once("error", onError);
|
|
898
987
|
stream.once("close", onClose);
|
|
988
|
+
terminalListeners.add(onTerminal);
|
|
899
989
|
});
|
|
900
990
|
},
|
|
901
991
|
end() {
|
|
@@ -906,7 +996,12 @@ export async function bidiRunStream(token, options) {
|
|
|
906
996
|
finishInbound();
|
|
907
997
|
stopInboundReader();
|
|
908
998
|
settleTerminal({ kind: "local-close" });
|
|
909
|
-
|
|
999
|
+
// A blocked write cannot finish gracefully; discard it rather than leave
|
|
1000
|
+
// its stream and session referenced after the drain has been rejected.
|
|
1001
|
+
if (backpressured)
|
|
1002
|
+
closeLocally();
|
|
1003
|
+
else
|
|
1004
|
+
stream.end();
|
|
910
1005
|
},
|
|
911
1006
|
async *frames() {
|
|
912
1007
|
try {
|
|
@@ -948,8 +1043,10 @@ export async function bidiRunStream(token, options) {
|
|
|
948
1043
|
finishInbound();
|
|
949
1044
|
stopInboundReader();
|
|
950
1045
|
settleTerminal({ kind: "local-close" });
|
|
951
|
-
stream.close();
|
|
952
1046
|
}
|
|
1047
|
+
// end() may already have half-closed the request. destroy() must still
|
|
1048
|
+
// reset the stream if the peer has left its response open.
|
|
1049
|
+
closeLocally();
|
|
953
1050
|
},
|
|
954
1051
|
isClosed() {
|
|
955
1052
|
return remotelyClosed || locallyClosed || stream.closed || stream.destroyed;
|
package/dist/usage.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export type CursorCacheDiagnosticStats = {
|
|
|
19
19
|
conversationGroupId?: string;
|
|
20
20
|
modelId?: string;
|
|
21
21
|
startedWithCheckpoint: boolean;
|
|
22
|
+
/** Why a Run started without a checkpoint: a reset reason, `ephemeral`, or `no-checkpoint`. */
|
|
23
|
+
coldReason?: string;
|
|
22
24
|
requestContextReused: boolean;
|
|
23
25
|
requestContextHash: string;
|
|
24
26
|
systemPromptHash?: string;
|
|
@@ -51,31 +53,49 @@ export declare function formatCursorTokenCategories(details: CursorConversationT
|
|
|
51
53
|
* does not expose per-model-call cache accounting on the Run stream.
|
|
52
54
|
*/
|
|
53
55
|
export declare function formatCursorCacheDiagnostics(counters: CursorUsageCounters, current: CursorConversationTokenDetails | undefined, prior: CursorConversationTokenDetails | undefined, stats: CursorCacheDiagnosticStats): string;
|
|
54
|
-
/** One-line proof that Cursor, AI SDK, and projected OpenCode totals agree. */
|
|
55
|
-
export declare function formatTurnUsageValidation(counters: CursorUsageCounters, usage: LanguageModelV3Usage, tokenDetails?: CursorConversationTokenDetails, contextSource?: CursorContextUsageSource): string;
|
|
56
|
-
/** OpenCode requires a usage object at every step boundary. */
|
|
57
|
-
export declare function emptyLanguageModelV3Usage(): LanguageModelV3Usage;
|
|
58
56
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* callers attach {@link OPENCODE_DISPLAY_ONLY_COST_METADATA} so getUsage
|
|
64
|
-
* reports $0 instead of billing the snapshot as a new prompt.
|
|
57
|
+
* One-line proof that Cursor, AI SDK, and projected OpenCode totals agree.
|
|
58
|
+
* `counterKind` names what `counters` hold so the line labels them truthfully:
|
|
59
|
+
* Cursor's TurnEnded request counters (`raw*`) or the occupancy-shaped counters
|
|
60
|
+
* of {@link occupancyValidationCounters} (`occupancy*`).
|
|
65
61
|
*/
|
|
66
|
-
export declare
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
export declare function formatTurnUsageValidation(counters: CursorUsageCounters, usage: LanguageModelV3Usage, tokenDetails?: CursorConversationTokenDetails, contextSource?: CursorContextUsageSource, counterKind?: "turn-ended" | "occupancy"): string;
|
|
63
|
+
/**
|
|
64
|
+
* The `finish:` trace. `v3*` is the usage sent to OpenCode. The second group is
|
|
65
|
+
* labelled by where its numbers come from: `raw*` are Cursor's TurnEnded
|
|
66
|
+
* request counters (whole Run), `occupancy*` the checkpoint snapshot a
|
|
67
|
+
* tool-call boundary sends (its cache split as billed: the previous step's
|
|
68
|
+
* context, see `src/billing.ts`), and
|
|
69
|
+
* `est*` the provider's char/4 estimate before any checkpoint arrived.
|
|
70
|
+
*/
|
|
71
|
+
export declare function formatFinishTrace(input: {
|
|
72
|
+
reason: string;
|
|
73
|
+
usage: LanguageModelV3Usage;
|
|
74
|
+
turnEnded?: CursorUsageCounters;
|
|
75
|
+
occupancy?: {
|
|
76
|
+
usedTokens: number;
|
|
77
|
+
priorUsedTokens: number;
|
|
69
78
|
};
|
|
70
|
-
|
|
79
|
+
estimate: CursorUsageCounters;
|
|
80
|
+
source: string;
|
|
81
|
+
}): string;
|
|
82
|
+
/**
|
|
83
|
+
* A warning when Cursor's checkpoint counts no rules although this Run sent the
|
|
84
|
+
* system-instructions rule: the host system context (AGENTS.md, skills,
|
|
85
|
+
* subagents) is then likely absent from the model's prompt. Undefined when the
|
|
86
|
+
* breakdown is missing or stale, or when rules were counted.
|
|
87
|
+
*/
|
|
88
|
+
export declare function missingRulesWarning(details: CursorConversationTokenDetails | undefined, sentRuleChars: number, conversationId: string): string | undefined;
|
|
89
|
+
/** OpenCode requires a usage object at every step boundary. */
|
|
90
|
+
export declare function emptyLanguageModelV3Usage(): LanguageModelV3Usage;
|
|
71
91
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
92
|
+
* Occupancy-shaped counters for {@link formatTurnUsageValidation}: `usedTokens`
|
|
93
|
+
* with `prior.usedTokens` of it cached, the shape every step finish sends
|
|
94
|
+
* (`shapeStepUsage` in `src/billing.ts`). Always validate occupancy finishes —
|
|
74
95
|
* including TurnEnded/stop — against these, not against aggregate TurnEnded
|
|
75
96
|
* request counters. Request cache ratios stay on `finish:` / cache diagnosis.
|
|
76
97
|
*/
|
|
77
98
|
export declare function occupancyValidationCounters(details: CursorConversationTokenDetails, prior?: CursorConversationTokenDetails): CursorUsageCounters;
|
|
78
|
-
export declare function occupancyUsageFromTokenDetails(details: CursorConversationTokenDetails, prior?: CursorConversationTokenDetails): LanguageModelV3Usage;
|
|
79
99
|
/** Project nested V3 usage into the common flat AI-SDK counter shape. */
|
|
80
100
|
export declare function flatUsageFromV3(usage: LanguageModelV3Usage): {
|
|
81
101
|
inputTokens: number;
|
package/dist/usage.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { currentCursorTokenBreakdown } from "./protocol/token-details.js";
|
|
1
2
|
/** Non-negative integer counter from a Cursor `turn_ended` field. */
|
|
2
3
|
export function turnEndedCounter(te, key) {
|
|
3
4
|
const value = te[key];
|
|
@@ -75,7 +76,7 @@ function usageRatio(part, total) {
|
|
|
75
76
|
return total > 0 ? `${(part / total * 100).toFixed(1)}%` : "n/a";
|
|
76
77
|
}
|
|
77
78
|
function categoryTokens(details) {
|
|
78
|
-
return new Map(details?.
|
|
79
|
+
return new Map(currentCursorTokenBreakdown(details)?.categories.map((category) => [
|
|
79
80
|
category.id || category.label || "(unnamed)",
|
|
80
81
|
category.estimatedTokens,
|
|
81
82
|
]) ?? []);
|
|
@@ -97,7 +98,7 @@ export function formatCursorCacheDiagnostics(counters, current, prior, stats) {
|
|
|
97
98
|
const rawUncached = Math.max(0, rawInput - rawRead - rawWrite);
|
|
98
99
|
const priorCategories = categoryTokens(prior);
|
|
99
100
|
const currentCategories = categoryTokens(current);
|
|
100
|
-
const categoriesComparable = !!prior
|
|
101
|
+
const categoriesComparable = !!currentCursorTokenBreakdown(prior) && !!currentCursorTokenBreakdown(current);
|
|
101
102
|
const categoryDelta = {};
|
|
102
103
|
let sameSizedCategoryTokens = 0;
|
|
103
104
|
if (categoriesComparable) {
|
|
@@ -132,6 +133,13 @@ export function formatCursorCacheDiagnostics(counters, current, prior, stats) {
|
|
|
132
133
|
&& toolsDelta !== 0
|
|
133
134
|
? "client-overlay-changed"
|
|
134
135
|
: "none";
|
|
136
|
+
// Occupancy measures context size, not cache reuse. A zero counter on an
|
|
137
|
+
// interaction turn cannot distinguish omitted accounting from a cache miss.
|
|
138
|
+
const interactionZeroRead = stats.startedWithCheckpoint
|
|
139
|
+
&& rawRead === 0
|
|
140
|
+
&& typeof prior?.usedTokens === "number"
|
|
141
|
+
&& prior.usedTokens > 0
|
|
142
|
+
&& (stats.createPlanInTurn === true || stats.switchModeInTurn === true);
|
|
135
143
|
return [
|
|
136
144
|
"cache diagnosis:",
|
|
137
145
|
`sessionKey=${stats.sessionKey ?? "-"}`,
|
|
@@ -139,6 +147,7 @@ export function formatCursorCacheDiagnostics(counters, current, prior, stats) {
|
|
|
139
147
|
`conversationGroupId=${stats.conversationGroupId ?? "-"}`,
|
|
140
148
|
`model=${stats.modelId ?? "-"}`,
|
|
141
149
|
`continuity=${continuity}`,
|
|
150
|
+
`coldReason=${continuity === "cold" ? stats.coldReason ?? "unknown" : "-"}`,
|
|
142
151
|
`rawInput=${rawInput}`,
|
|
143
152
|
`rawCacheRead=${rawRead}`,
|
|
144
153
|
`rawCacheWrite=${rawWrite}`,
|
|
@@ -148,7 +157,9 @@ export function formatCursorCacheDiagnostics(counters, current, prior, stats) {
|
|
|
148
157
|
`priorContext=${prior?.usedTokens ?? "unavailable"}`,
|
|
149
158
|
`currentContext=${current?.usedTokens ?? "unavailable"}`,
|
|
150
159
|
`contextDelta=${contextDelta ?? "unavailable"}`,
|
|
151
|
-
|
|
160
|
+
// Several model calls in one Run each re-read the prefix, so this multiple
|
|
161
|
+
// routinely exceeds 1 and is not a percentage.
|
|
162
|
+
`rawReadVsPriorContext=${prior && prior.usedTokens > 0 ? `${(rawRead / prior.usedTokens).toFixed(2)}x` : "n/a"}`,
|
|
152
163
|
`sameSizedCategoryTokens=${categoriesComparable ? sameSizedCategoryTokens : "unavailable"}`,
|
|
153
164
|
`categoryDelta=${categoriesComparable && Object.keys(categoryDelta).length > 0 ? JSON.stringify(categoryDelta) : "unavailable"}`,
|
|
154
165
|
`toolsCategoryChurn=${toolsCategoryChurn}`,
|
|
@@ -164,11 +175,23 @@ export function formatCursorCacheDiagnostics(counters, current, prior, stats) {
|
|
|
164
175
|
`execRequests=${stats.execRequests}`,
|
|
165
176
|
`createPlanInTurn=${stats.createPlanInTurn === true}`,
|
|
166
177
|
`switchModeInTurn=${stats.switchModeInTurn === true}`,
|
|
178
|
+
...(interactionZeroRead
|
|
179
|
+
? [
|
|
180
|
+
"turnEndedCacheRead=zero-interaction",
|
|
181
|
+
"cacheReuseEvidence=unavailable",
|
|
182
|
+
]
|
|
183
|
+
: []),
|
|
167
184
|
"perModelCallCache=unavailable",
|
|
168
185
|
].join(" ");
|
|
169
186
|
}
|
|
170
|
-
/**
|
|
171
|
-
|
|
187
|
+
/**
|
|
188
|
+
* One-line proof that Cursor, AI SDK, and projected OpenCode totals agree.
|
|
189
|
+
* `counterKind` names what `counters` hold so the line labels them truthfully:
|
|
190
|
+
* Cursor's TurnEnded request counters (`raw*`) or the occupancy-shaped counters
|
|
191
|
+
* of {@link occupancyValidationCounters} (`occupancy*`).
|
|
192
|
+
*/
|
|
193
|
+
export function formatTurnUsageValidation(counters, usage, tokenDetails, contextSource, counterKind = "turn-ended") {
|
|
194
|
+
const counterLabel = counterKind === "occupancy" ? "occupancy" : "raw";
|
|
172
195
|
const input = usageCount(usage.inputTokens.total);
|
|
173
196
|
const noCache = usageCount(usage.inputTokens.noCache);
|
|
174
197
|
const cacheRead = usageCount(usage.inputTokens.cacheRead);
|
|
@@ -189,8 +212,9 @@ export function formatTurnUsageValidation(counters, usage, tokenDetails, context
|
|
|
189
212
|
const breakdown = tokenDetails?.breakdown;
|
|
190
213
|
const categorySum = breakdown?.categories.reduce((sum, category) => sum + category.estimatedTokens, 0);
|
|
191
214
|
const breakdownMatch = breakdown && categorySum !== undefined
|
|
192
|
-
? breakdown.totalUsedTokens
|
|
193
|
-
|
|
215
|
+
? categorySum !== breakdown.totalUsedTokens
|
|
216
|
+
? false
|
|
217
|
+
: currentCursorTokenBreakdown(tokenDetails) ? true : "stale"
|
|
194
218
|
: undefined;
|
|
195
219
|
const rawCached = counters.cacheRead + counters.cacheWrite;
|
|
196
220
|
const sentCached = cacheRead + cacheWrite;
|
|
@@ -206,9 +230,10 @@ export function formatTurnUsageValidation(counters, usage, tokenDetails, context
|
|
|
206
230
|
const status = input === inputParts &&
|
|
207
231
|
output === outputParts &&
|
|
208
232
|
projectedOpenCodeTotal === sentTotal &&
|
|
233
|
+
(counterKind !== "occupancy" || rawTotal === sentTotal) &&
|
|
209
234
|
(cacheRatioMatch ?? true) &&
|
|
210
235
|
(!tokenDetails || sentTotal === tokenDetails.usedTokens) &&
|
|
211
|
-
|
|
236
|
+
breakdownMatch !== false
|
|
212
237
|
? "ok"
|
|
213
238
|
: "mismatch";
|
|
214
239
|
return [
|
|
@@ -216,7 +241,7 @@ export function formatTurnUsageValidation(counters, usage, tokenDetails, context
|
|
|
216
241
|
`status=${status}`,
|
|
217
242
|
`source=${tokenDetails ? contextSource ?? "checkpoint-current-run" : "unavailable"}`,
|
|
218
243
|
`cursor=${cursor}`,
|
|
219
|
-
|
|
244
|
+
`${counterLabel}Total=${rawTotal}`,
|
|
220
245
|
`sentTotal=${sentTotal}`,
|
|
221
246
|
`totalMatch=${totalMatch}`,
|
|
222
247
|
`input=${input}`,
|
|
@@ -230,11 +255,53 @@ export function formatTurnUsageValidation(counters, usage, tokenDetails, context
|
|
|
230
255
|
`breakdownTotal=${breakdown?.totalUsedTokens ?? "unavailable"}`,
|
|
231
256
|
`categorySum=${categorySum ?? "unavailable"}`,
|
|
232
257
|
`breakdownMatch=${breakdownMatch ?? "unavailable"}`,
|
|
233
|
-
|
|
258
|
+
`${counterLabel}CachedRatio=${usageRatio(rawCached, counters.inputTokens)}`,
|
|
234
259
|
`sentCachedRatio=${usageRatio(sentCached, input)}`,
|
|
235
260
|
`cacheRatioMatch=${cacheRatioMatch ?? "unavailable"}`,
|
|
236
261
|
].join(" ");
|
|
237
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* The `finish:` trace. `v3*` is the usage sent to OpenCode. The second group is
|
|
265
|
+
* labelled by where its numbers come from: `raw*` are Cursor's TurnEnded
|
|
266
|
+
* request counters (whole Run), `occupancy*` the checkpoint snapshot a
|
|
267
|
+
* tool-call boundary sends (its cache split as billed: the previous step's
|
|
268
|
+
* context, see `src/billing.ts`), and
|
|
269
|
+
* `est*` the provider's char/4 estimate before any checkpoint arrived.
|
|
270
|
+
*/
|
|
271
|
+
export function formatFinishTrace(input) {
|
|
272
|
+
const { usage, turnEnded, occupancy, estimate } = input;
|
|
273
|
+
const counters = turnEnded
|
|
274
|
+
? `rawIn=${turnEnded.inputTokens} rawOut=${turnEnded.outputTokens} ` +
|
|
275
|
+
`rawCacheRead=${turnEnded.cacheRead} rawCacheWrite=${turnEnded.cacheWrite} ` +
|
|
276
|
+
(occupancy ? `occupancyPrefixCache=${occupancy.priorUsedTokens} ` : "")
|
|
277
|
+
: occupancy
|
|
278
|
+
? `occupancyIn=${Math.max(0, occupancy.usedTokens - 1)} occupancyOut=${occupancy.usedTokens > 0 ? 1 : 0} ` +
|
|
279
|
+
`occupancyCacheRead=${usage.inputTokens?.cacheRead ?? 0} ` +
|
|
280
|
+
`occupancyCacheWrite=${usage.inputTokens?.cacheWrite ?? 0} `
|
|
281
|
+
: `estIn=${estimate.inputTokens} estOut=${estimate.outputTokens} ` +
|
|
282
|
+
`estCacheRead=${estimate.cacheRead} estCacheWrite=${estimate.cacheWrite} `;
|
|
283
|
+
return `finish: reason=${input.reason} ` +
|
|
284
|
+
`v3In=${usage.inputTokens?.total ?? 0} v3Out=${usage.outputTokens?.total ?? 0} ` +
|
|
285
|
+
`v3CacheRead=${usage.inputTokens?.cacheRead ?? 0} v3CacheWrite=${usage.inputTokens?.cacheWrite ?? 0} ` +
|
|
286
|
+
`v3Reasoning=${usage.outputTokens?.reasoning ?? 0} ` +
|
|
287
|
+
counters +
|
|
288
|
+
`source=${input.source}`;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* A warning when Cursor's checkpoint counts no rules although this Run sent the
|
|
292
|
+
* system-instructions rule: the host system context (AGENTS.md, skills,
|
|
293
|
+
* subagents) is then likely absent from the model's prompt. Undefined when the
|
|
294
|
+
* breakdown is missing or stale, or when rules were counted.
|
|
295
|
+
*/
|
|
296
|
+
export function missingRulesWarning(details, sentRuleChars, conversationId) {
|
|
297
|
+
if (sentRuleChars <= 0)
|
|
298
|
+
return undefined;
|
|
299
|
+
const rules = currentCursorTokenBreakdown(details)?.categories.find((category) => category.id === "rules");
|
|
300
|
+
if (!rules || rules.estimatedTokens > 0)
|
|
301
|
+
return undefined;
|
|
302
|
+
return `context warning: Cursor counted rules=0 although this Run sent the ${sentRuleChars}-char ` +
|
|
303
|
+
`system-instructions rule conversationId=${conversationId} — host instructions may not reach the model`;
|
|
304
|
+
}
|
|
238
305
|
/** OpenCode requires a usage object at every step boundary. */
|
|
239
306
|
export function emptyLanguageModelV3Usage() {
|
|
240
307
|
return buildLanguageModelV3UsageFromCounters({
|
|
@@ -246,41 +313,25 @@ export function emptyLanguageModelV3Usage() {
|
|
|
246
313
|
});
|
|
247
314
|
}
|
|
248
315
|
/**
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* therefore sent as a snapshot with `output=1` so the footer accepts it, and
|
|
253
|
-
* callers attach {@link OPENCODE_DISPLAY_ONLY_COST_METADATA} so getUsage
|
|
254
|
-
* reports $0 instead of billing the snapshot as a new prompt.
|
|
255
|
-
*/
|
|
256
|
-
export const OPENCODE_DISPLAY_ONLY_COST_METADATA = {
|
|
257
|
-
copilot: { totalNanoAiu: 0 },
|
|
258
|
-
};
|
|
259
|
-
/**
|
|
260
|
-
* Counters that mirror {@link occupancyUsageFromTokenDetails} for
|
|
261
|
-
* {@link formatTurnUsageValidation}. Always validate occupancy finishes —
|
|
316
|
+
* Occupancy-shaped counters for {@link formatTurnUsageValidation}: `usedTokens`
|
|
317
|
+
* with `prior.usedTokens` of it cached, the shape every step finish sends
|
|
318
|
+
* (`shapeStepUsage` in `src/billing.ts`). Always validate occupancy finishes —
|
|
262
319
|
* including TurnEnded/stop — against these, not against aggregate TurnEnded
|
|
263
320
|
* request counters. Request cache ratios stay on `finish:` / cache diagnosis.
|
|
264
321
|
*/
|
|
265
322
|
export function occupancyValidationCounters(details, prior) {
|
|
266
323
|
const used = Math.max(0, Math.trunc(details.usedTokens));
|
|
324
|
+
const input = Math.max(0, used - 1);
|
|
267
325
|
return {
|
|
268
|
-
inputTokens:
|
|
326
|
+
inputTokens: input,
|
|
269
327
|
outputTokens: used > 0 ? 1 : 0,
|
|
270
|
-
|
|
328
|
+
// Cursor can shrink the context between checkpoints; the prior prefix
|
|
329
|
+
// cannot be larger than what is in context now.
|
|
330
|
+
cacheRead: Math.min(input, Math.max(0, Math.trunc(prior?.usedTokens ?? 0))),
|
|
271
331
|
cacheWrite: 0,
|
|
272
332
|
reasoningTokens: 0,
|
|
273
333
|
};
|
|
274
334
|
}
|
|
275
|
-
export function occupancyUsageFromTokenDetails(details, prior) {
|
|
276
|
-
const used = Math.max(0, Math.trunc(details.usedTokens));
|
|
277
|
-
if (used <= 0)
|
|
278
|
-
return emptyLanguageModelV3Usage();
|
|
279
|
-
return buildLanguageModelV3UsageFromCounters(occupancyValidationCounters(details, prior), {
|
|
280
|
-
contextTotalTokens: used,
|
|
281
|
-
priorContextTokens: prior?.usedTokens,
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
335
|
/** Project nested V3 usage into the common flat AI-SDK counter shape. */
|
|
285
336
|
export function flatUsageFromV3(usage) {
|
|
286
337
|
const input = usage.inputTokens;
|