cursor-opencode-provider 0.8.1 → 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 +95 -0
- package/README.md +204 -37
- package/SECURITY.md +20 -8
- 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 +1 -0
- package/dist/context/build.js +32 -5
- package/dist/context/env.js +27 -3
- package/dist/context/epoch.js +9 -5
- package/dist/context/git.d.ts +5 -2
- package/dist/context/git.js +17 -7
- package/dist/context/paths.d.ts +1 -1
- package/dist/context/paths.js +7 -4
- 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-interrupt.d.ts +12 -0
- package/dist/host-interrupt.js +18 -0
- package/dist/host-steer.d.ts +71 -0
- package/dist/host-steer.js +232 -0
- package/dist/image-input.js +4 -3
- package/dist/index.d.ts +2 -2
- package/dist/language-model.d.ts +114 -23
- package/dist/language-model.js +2290 -469
- package/dist/lost-request.d.ts +39 -0
- package/dist/lost-request.js +111 -0
- package/dist/model-config.d.ts +3 -1
- package/dist/model-config.js +50 -29
- package/dist/models.d.ts +26 -3
- package/dist/models.js +101 -45
- package/dist/opencode2/tool-results.d.ts +18 -0
- package/dist/opencode2/tool-results.js +66 -0
- package/dist/plugin-opencode2.js +185 -25
- package/dist/plugin.js +28 -5
- package/dist/pricing-data.d.ts +26 -2
- package/dist/pricing-data.js +26 -2
- 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/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/host-notes.d.ts +31 -0
- package/dist/protocol/host-notes.js +85 -0
- package/dist/protocol/messages.js +91 -8
- package/dist/protocol/request.d.ts +35 -19
- package/dist/protocol/request.js +53 -28
- package/dist/protocol/sent-history-images.d.ts +49 -0
- package/dist/protocol/sent-history-images.js +102 -0
- package/dist/protocol/tool-call-bridge.d.ts +2 -0
- package/dist/protocol/tool-call-bridge.js +10 -0
- package/dist/protocol/tools.d.ts +74 -8
- package/dist/protocol/tools.js +431 -141
- package/dist/protocol/turn-provenance.d.ts +19 -4
- package/dist/protocol/turn-provenance.js +77 -20
- package/dist/replay-safety.d.ts +16 -4
- package/dist/replay-safety.js +34 -25
- 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 +94 -9
- package/dist/session.js +153 -34
- 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/transport/connect.d.ts +8 -1
- package/dist/transport/connect.js +103 -23
- package/dist/usage.d.ts +37 -17
- package/dist/usage.js +63 -29
- package/package.json +3 -2
- package/dist/protocol/thinking.d.ts +0 -15
- package/dist/protocol/thinking.js +0 -17
|
@@ -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;
|
|
@@ -78,6 +83,8 @@ export declare function cacheHttp2SessionForTests(origin: string, session: http2
|
|
|
78
83
|
export declare function getSession(baseURL: string, options?: {
|
|
79
84
|
pingTimeoutMs?: number;
|
|
80
85
|
}): Promise<http2.ClientHttp2Session>;
|
|
86
|
+
export declare function frameArrival(frame: object): number | undefined;
|
|
87
|
+
export declare function lastFrameArrival(): number;
|
|
81
88
|
export declare function bidiRunStream(token: string, options: {
|
|
82
89
|
signal?: AbortSignal;
|
|
83
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
|
}
|
|
@@ -272,6 +287,29 @@ function retireCachedSession(origin, session) {
|
|
|
272
287
|
}
|
|
273
288
|
catch { /* already closed */ }
|
|
274
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
|
+
}
|
|
275
313
|
/** Test cleanup for local HTTP/2 fixtures; production sessions stay process-cached. */
|
|
276
314
|
export function closeCachedHttp2SessionsForTests() {
|
|
277
315
|
for (const session of _http2Sessions.values()) {
|
|
@@ -369,9 +407,10 @@ export function installSessionInvalidationForTests(origin, session) {
|
|
|
369
407
|
}
|
|
370
408
|
/** Node-runtime regression hook: cache a connected local session for `origin`. */
|
|
371
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)?.();
|
|
372
412
|
installSessionInvalidation(origin, session);
|
|
373
|
-
|
|
374
|
-
_http2Sessions.set(origin, session);
|
|
413
|
+
cacheReadySession(origin, session);
|
|
375
414
|
}
|
|
376
415
|
function invalidateSession(origin, session) {
|
|
377
416
|
dropSession(origin, session);
|
|
@@ -486,8 +525,7 @@ function connectSession(origin) {
|
|
|
486
525
|
}
|
|
487
526
|
// The TLS socket (if any) is owned by the HTTP/2 session from here on.
|
|
488
527
|
tunnelSocket = undefined;
|
|
489
|
-
|
|
490
|
-
_http2Sessions.set(origin, session);
|
|
528
|
+
cacheReadySession(origin, session);
|
|
491
529
|
trace(`h2 session connected: origin=${origin}`);
|
|
492
530
|
resolve(session);
|
|
493
531
|
};
|
|
@@ -518,11 +556,12 @@ function connectSession(origin) {
|
|
|
518
556
|
}
|
|
519
557
|
tunnelSocket = tlsSocket;
|
|
520
558
|
session = http2.connect(origin, {
|
|
559
|
+
...RUN_SESSION_OPTIONS,
|
|
521
560
|
createConnection: () => tlsSocket,
|
|
522
561
|
});
|
|
523
562
|
}
|
|
524
563
|
else {
|
|
525
|
-
session = http2.connect(origin);
|
|
564
|
+
session = http2.connect(origin, RUN_SESSION_OPTIONS);
|
|
526
565
|
}
|
|
527
566
|
if (settled) {
|
|
528
567
|
try {
|
|
@@ -558,7 +597,13 @@ export function getSession(baseURL, options = {}) {
|
|
|
558
597
|
}
|
|
559
598
|
else {
|
|
560
599
|
try {
|
|
561
|
-
|
|
600
|
+
holdHttp2Session(existing);
|
|
601
|
+
try {
|
|
602
|
+
await validateCachedSession(existing, pingTimeoutMs);
|
|
603
|
+
}
|
|
604
|
+
finally {
|
|
605
|
+
releaseHttp2Session(existing);
|
|
606
|
+
}
|
|
562
607
|
if (_http2Sessions.get(origin) === existing && !existing.destroyed && !existing.closed) {
|
|
563
608
|
trace(`h2 cached session ping ok: origin=${origin}`);
|
|
564
609
|
return existing;
|
|
@@ -582,6 +627,15 @@ export function getSession(baseURL, options = {}) {
|
|
|
582
627
|
promise.then(cleanup, cleanup);
|
|
583
628
|
return promise;
|
|
584
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
|
+
}
|
|
585
639
|
export async function bidiRunStream(token, options) {
|
|
586
640
|
const origin = resolveAgentOrigin(options.baseURL);
|
|
587
641
|
const readIdleMs = timeoutMs("Cursor provider readIdleMs", options.readIdleMs, DEFAULT_READ_IDLE_MS);
|
|
@@ -604,6 +658,7 @@ export async function bidiRunStream(token, options) {
|
|
|
604
658
|
const stream = session.request(headers, {
|
|
605
659
|
endStream: false,
|
|
606
660
|
});
|
|
661
|
+
holdHttp2Session(session);
|
|
607
662
|
let writable = true;
|
|
608
663
|
let locallyClosed = false;
|
|
609
664
|
let remotelyClosed = false;
|
|
@@ -643,6 +698,14 @@ export async function bidiRunStream(token, options) {
|
|
|
643
698
|
}
|
|
644
699
|
return undefined;
|
|
645
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
|
+
};
|
|
646
709
|
const settleTerminal = (event) => {
|
|
647
710
|
if (terminalEvent)
|
|
648
711
|
return;
|
|
@@ -768,8 +831,10 @@ export async function bidiRunStream(token, options) {
|
|
|
768
831
|
pendingChunks.length = 0;
|
|
769
832
|
if (consumed < merged.length)
|
|
770
833
|
pendingChunks.push(merged.subarray(consumed));
|
|
771
|
-
for (const frame of parsed)
|
|
834
|
+
for (const frame of parsed) {
|
|
835
|
+
frameArrivals.set(frame, ++lastArrival);
|
|
772
836
|
enqueueFrame(frame);
|
|
837
|
+
}
|
|
773
838
|
};
|
|
774
839
|
const startInboundReader = () => {
|
|
775
840
|
if (inboundReaderStarted || locallyClosed || remotelyClosed || streamFailure)
|
|
@@ -812,6 +877,7 @@ export async function bidiRunStream(token, options) {
|
|
|
812
877
|
});
|
|
813
878
|
stream.on("close", () => {
|
|
814
879
|
writable = false;
|
|
880
|
+
releaseHttp2Session(session);
|
|
815
881
|
remotelyClosed = !locallyClosed;
|
|
816
882
|
if (remotelyClosed) {
|
|
817
883
|
dropSession(origin, session);
|
|
@@ -827,6 +893,16 @@ export async function bidiRunStream(token, options) {
|
|
|
827
893
|
`err=${rawStreamError instanceof Error ? rawStreamError.message : "none"})`);
|
|
828
894
|
scheduleTerminalSettlement();
|
|
829
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
|
+
};
|
|
830
906
|
if (abortSignal) {
|
|
831
907
|
abortHandler = () => {
|
|
832
908
|
if (!locallyClosed) {
|
|
@@ -837,7 +913,7 @@ export async function bidiRunStream(token, options) {
|
|
|
837
913
|
finishInbound();
|
|
838
914
|
stopInboundReader();
|
|
839
915
|
settleTerminal({ kind: "local-close" });
|
|
840
|
-
|
|
916
|
+
closeLocally();
|
|
841
917
|
}
|
|
842
918
|
};
|
|
843
919
|
abortSignal.addEventListener("abort", abortHandler, { once: true });
|
|
@@ -868,20 +944,21 @@ export async function bidiRunStream(token, options) {
|
|
|
868
944
|
throw streamFailure;
|
|
869
945
|
}
|
|
870
946
|
},
|
|
871
|
-
waitForDrain(
|
|
947
|
+
waitForDrain() {
|
|
872
948
|
if (!backpressured)
|
|
873
949
|
return Promise.resolve();
|
|
874
|
-
|
|
950
|
+
if (terminalEvent)
|
|
951
|
+
return Promise.reject(drainTerminalError(terminalEvent));
|
|
875
952
|
return new Promise((resolve, reject) => {
|
|
876
953
|
let settled = false;
|
|
877
954
|
const finish = (error) => {
|
|
878
955
|
if (settled)
|
|
879
956
|
return;
|
|
880
957
|
settled = true;
|
|
881
|
-
clearTimeout(timer);
|
|
882
958
|
stream.removeListener("drain", onDrain);
|
|
883
959
|
stream.removeListener("error", onError);
|
|
884
960
|
stream.removeListener("close", onClose);
|
|
961
|
+
terminalListeners.delete(onTerminal);
|
|
885
962
|
if (error)
|
|
886
963
|
reject(error);
|
|
887
964
|
else
|
|
@@ -901,18 +978,14 @@ export async function bidiRunStream(token, options) {
|
|
|
901
978
|
? new CursorLocalCancellationError("Cursor Run stream closed locally during drain")
|
|
902
979
|
: observedFailure() ?? new CursorRunInterruptedError("Cursor Run stream closed before drain"));
|
|
903
980
|
};
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
stream.destroy(streamFailure);
|
|
909
|
-
}
|
|
910
|
-
catch { /* already closing */ }
|
|
911
|
-
}, drainTimeoutMs);
|
|
912
|
-
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));
|
|
913
985
|
stream.once("drain", onDrain);
|
|
914
986
|
stream.once("error", onError);
|
|
915
987
|
stream.once("close", onClose);
|
|
988
|
+
terminalListeners.add(onTerminal);
|
|
916
989
|
});
|
|
917
990
|
},
|
|
918
991
|
end() {
|
|
@@ -923,7 +996,12 @@ export async function bidiRunStream(token, options) {
|
|
|
923
996
|
finishInbound();
|
|
924
997
|
stopInboundReader();
|
|
925
998
|
settleTerminal({ kind: "local-close" });
|
|
926
|
-
|
|
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();
|
|
927
1005
|
},
|
|
928
1006
|
async *frames() {
|
|
929
1007
|
try {
|
|
@@ -965,8 +1043,10 @@ export async function bidiRunStream(token, options) {
|
|
|
965
1043
|
finishInbound();
|
|
966
1044
|
stopInboundReader();
|
|
967
1045
|
settleTerminal({ kind: "local-close" });
|
|
968
|
-
stream.close();
|
|
969
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();
|
|
970
1050
|
},
|
|
971
1051
|
isClosed() {
|
|
972
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
|
@@ -147,6 +147,7 @@ export function formatCursorCacheDiagnostics(counters, current, prior, stats) {
|
|
|
147
147
|
`conversationGroupId=${stats.conversationGroupId ?? "-"}`,
|
|
148
148
|
`model=${stats.modelId ?? "-"}`,
|
|
149
149
|
`continuity=${continuity}`,
|
|
150
|
+
`coldReason=${continuity === "cold" ? stats.coldReason ?? "unknown" : "-"}`,
|
|
150
151
|
`rawInput=${rawInput}`,
|
|
151
152
|
`rawCacheRead=${rawRead}`,
|
|
152
153
|
`rawCacheWrite=${rawWrite}`,
|
|
@@ -156,7 +157,9 @@ export function formatCursorCacheDiagnostics(counters, current, prior, stats) {
|
|
|
156
157
|
`priorContext=${prior?.usedTokens ?? "unavailable"}`,
|
|
157
158
|
`currentContext=${current?.usedTokens ?? "unavailable"}`,
|
|
158
159
|
`contextDelta=${contextDelta ?? "unavailable"}`,
|
|
159
|
-
|
|
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"}`,
|
|
160
163
|
`sameSizedCategoryTokens=${categoriesComparable ? sameSizedCategoryTokens : "unavailable"}`,
|
|
161
164
|
`categoryDelta=${categoriesComparable && Object.keys(categoryDelta).length > 0 ? JSON.stringify(categoryDelta) : "unavailable"}`,
|
|
162
165
|
`toolsCategoryChurn=${toolsCategoryChurn}`,
|
|
@@ -181,8 +184,14 @@ export function formatCursorCacheDiagnostics(counters, current, prior, stats) {
|
|
|
181
184
|
"perModelCallCache=unavailable",
|
|
182
185
|
].join(" ");
|
|
183
186
|
}
|
|
184
|
-
/**
|
|
185
|
-
|
|
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";
|
|
186
195
|
const input = usageCount(usage.inputTokens.total);
|
|
187
196
|
const noCache = usageCount(usage.inputTokens.noCache);
|
|
188
197
|
const cacheRead = usageCount(usage.inputTokens.cacheRead);
|
|
@@ -221,6 +230,7 @@ export function formatTurnUsageValidation(counters, usage, tokenDetails, context
|
|
|
221
230
|
const status = input === inputParts &&
|
|
222
231
|
output === outputParts &&
|
|
223
232
|
projectedOpenCodeTotal === sentTotal &&
|
|
233
|
+
(counterKind !== "occupancy" || rawTotal === sentTotal) &&
|
|
224
234
|
(cacheRatioMatch ?? true) &&
|
|
225
235
|
(!tokenDetails || sentTotal === tokenDetails.usedTokens) &&
|
|
226
236
|
breakdownMatch !== false
|
|
@@ -231,7 +241,7 @@ export function formatTurnUsageValidation(counters, usage, tokenDetails, context
|
|
|
231
241
|
`status=${status}`,
|
|
232
242
|
`source=${tokenDetails ? contextSource ?? "checkpoint-current-run" : "unavailable"}`,
|
|
233
243
|
`cursor=${cursor}`,
|
|
234
|
-
|
|
244
|
+
`${counterLabel}Total=${rawTotal}`,
|
|
235
245
|
`sentTotal=${sentTotal}`,
|
|
236
246
|
`totalMatch=${totalMatch}`,
|
|
237
247
|
`input=${input}`,
|
|
@@ -245,11 +255,53 @@ export function formatTurnUsageValidation(counters, usage, tokenDetails, context
|
|
|
245
255
|
`breakdownTotal=${breakdown?.totalUsedTokens ?? "unavailable"}`,
|
|
246
256
|
`categorySum=${categorySum ?? "unavailable"}`,
|
|
247
257
|
`breakdownMatch=${breakdownMatch ?? "unavailable"}`,
|
|
248
|
-
|
|
258
|
+
`${counterLabel}CachedRatio=${usageRatio(rawCached, counters.inputTokens)}`,
|
|
249
259
|
`sentCachedRatio=${usageRatio(sentCached, input)}`,
|
|
250
260
|
`cacheRatioMatch=${cacheRatioMatch ?? "unavailable"}`,
|
|
251
261
|
].join(" ");
|
|
252
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
|
+
}
|
|
253
305
|
/** OpenCode requires a usage object at every step boundary. */
|
|
254
306
|
export function emptyLanguageModelV3Usage() {
|
|
255
307
|
return buildLanguageModelV3UsageFromCounters({
|
|
@@ -261,43 +313,25 @@ export function emptyLanguageModelV3Usage() {
|
|
|
261
313
|
});
|
|
262
314
|
}
|
|
263
315
|
/**
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
* therefore sent as a snapshot with `output=1` so the footer accepts it, and
|
|
268
|
-
* callers attach {@link OPENCODE_DISPLAY_ONLY_COST_METADATA} so getUsage
|
|
269
|
-
* reports $0 instead of billing the snapshot as a new prompt.
|
|
270
|
-
*/
|
|
271
|
-
export const OPENCODE_DISPLAY_ONLY_COST_METADATA = {
|
|
272
|
-
copilot: { totalNanoAiu: 0 },
|
|
273
|
-
};
|
|
274
|
-
/**
|
|
275
|
-
* Counters that mirror {@link occupancyUsageFromTokenDetails} for
|
|
276
|
-
* {@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 —
|
|
277
319
|
* including TurnEnded/stop — against these, not against aggregate TurnEnded
|
|
278
320
|
* request counters. Request cache ratios stay on `finish:` / cache diagnosis.
|
|
279
321
|
*/
|
|
280
322
|
export function occupancyValidationCounters(details, prior) {
|
|
281
323
|
const used = Math.max(0, Math.trunc(details.usedTokens));
|
|
324
|
+
const input = Math.max(0, used - 1);
|
|
282
325
|
return {
|
|
283
|
-
inputTokens:
|
|
326
|
+
inputTokens: input,
|
|
284
327
|
outputTokens: used > 0 ? 1 : 0,
|
|
285
328
|
// Cursor can shrink the context between checkpoints; the prior prefix
|
|
286
329
|
// cannot be larger than what is in context now.
|
|
287
|
-
cacheRead: Math.min(
|
|
330
|
+
cacheRead: Math.min(input, Math.max(0, Math.trunc(prior?.usedTokens ?? 0))),
|
|
288
331
|
cacheWrite: 0,
|
|
289
332
|
reasoningTokens: 0,
|
|
290
333
|
};
|
|
291
334
|
}
|
|
292
|
-
export function occupancyUsageFromTokenDetails(details, prior) {
|
|
293
|
-
const used = Math.max(0, Math.trunc(details.usedTokens));
|
|
294
|
-
if (used <= 0)
|
|
295
|
-
return emptyLanguageModelV3Usage();
|
|
296
|
-
return buildLanguageModelV3UsageFromCounters(occupancyValidationCounters(details, prior), {
|
|
297
|
-
contextTotalTokens: used,
|
|
298
|
-
priorContextTokens: prior?.usedTokens,
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
335
|
/** Project nested V3 usage into the common flat AI-SDK counter shape. */
|
|
302
336
|
export function flatUsageFromV3(usage) {
|
|
303
337
|
const input = usage.inputTokens;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-opencode-provider",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "Use Cursor subscription models from OpenCode via Cursor's Connect-RPC agent protocol",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -77,7 +77,8 @@
|
|
|
77
77
|
"build": "tsc",
|
|
78
78
|
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
79
79
|
"test": "bun test",
|
|
80
|
-
"test:
|
|
80
|
+
"test:coverage": "bun test --coverage --coverage-reporter=lcov && bun scripts/strip-copied-coverage.ts",
|
|
81
|
+
"test:node-http2": "bun run build && node test/node-http2-cleanup.mjs && node test/node-http2-drain.mjs && node test/node-http2-session-memory.mjs",
|
|
81
82
|
"test:watch": "bun test --watch",
|
|
82
83
|
"generate:pricing": "bun run scripts/generate-cursor-pricing.ts",
|
|
83
84
|
"generate:pricing:check": "bun run scripts/generate-cursor-pricing.ts -- --check",
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export type EffortConfig = {
|
|
2
|
-
reasoningEffort?: string;
|
|
3
|
-
maxMode?: boolean;
|
|
4
|
-
};
|
|
5
|
-
/**
|
|
6
|
-
* Build RequestedModel parameter list from a model's variant parameters
|
|
7
|
-
* and optional overrides from providerOptions.
|
|
8
|
-
*/
|
|
9
|
-
export declare function buildRequestedModelParams(variantParameters: Array<{
|
|
10
|
-
id: string;
|
|
11
|
-
value: string;
|
|
12
|
-
}>, options?: EffortConfig): Array<{
|
|
13
|
-
id: string;
|
|
14
|
-
value: string;
|
|
15
|
-
}>;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// Reasoning effort / max mode → RequestedModel parameters
|
|
2
|
-
/**
|
|
3
|
-
* Build RequestedModel parameter list from a model's variant parameters
|
|
4
|
-
* and optional overrides from providerOptions.
|
|
5
|
-
*/
|
|
6
|
-
export function buildRequestedModelParams(variantParameters, options) {
|
|
7
|
-
// Start with variant's parameters as base
|
|
8
|
-
const params = variantParameters.map((p) => ({ ...p }));
|
|
9
|
-
// Override effort if specified
|
|
10
|
-
if (options?.reasoningEffort) {
|
|
11
|
-
const effortIdx = params.findIndex((p) => p.id === "effort" || p.id === "reasoning");
|
|
12
|
-
if (effortIdx >= 0) {
|
|
13
|
-
params[effortIdx] = { ...params[effortIdx], value: options.reasoningEffort };
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
return params;
|
|
17
|
-
}
|