cursor-opencode-provider 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -12
- package/dist/context/build.d.ts +8 -0
- package/dist/context/build.js +72 -23
- package/dist/context/frozen.d.ts +29 -0
- package/dist/context/frozen.js +168 -0
- package/dist/context/index.d.ts +2 -1
- package/dist/context/index.js +2 -1
- package/dist/context/rules.d.ts +1 -0
- package/dist/context/rules.js +1 -1
- package/dist/language-model.d.ts +10 -9
- package/dist/language-model.js +390 -112
- package/dist/pricing-data.d.ts +6 -0
- package/dist/pricing-data.js +6 -0
- package/dist/pricing.d.ts +1 -1
- package/dist/pricing.js +1 -1
- package/dist/protocol/blob-reachability.d.ts +15 -0
- package/dist/protocol/blob-reachability.js +140 -0
- package/dist/protocol/blob-store.d.ts +37 -0
- package/dist/protocol/blob-store.js +115 -0
- package/dist/protocol/conversation-bind.d.ts +13 -0
- package/dist/protocol/conversation-bind.js +27 -1
- package/dist/protocol/conversation-persistence.d.ts +32 -0
- package/dist/protocol/conversation-persistence.js +504 -0
- package/dist/protocol/conversation-state.d.ts +16 -0
- package/dist/protocol/conversation-state.js +62 -0
- package/dist/protocol/kv.d.ts +1 -0
- package/dist/protocol/kv.js +2 -0
- package/dist/protocol/messages.d.ts +2 -0
- package/dist/protocol/messages.js +30 -2
- package/dist/protocol/request.d.ts +2 -0
- package/dist/protocol/request.js +2 -0
- package/dist/protocol/token-details.d.ts +33 -0
- package/dist/protocol/token-details.js +155 -0
- package/dist/protocol/tools.d.ts +17 -0
- package/dist/protocol/tools.js +76 -0
- package/dist/session.d.ts +39 -11
- package/dist/shared.d.ts +3 -0
- package/dist/shared.js +3 -0
- package/dist/usage.d.ts +42 -22
- package/dist/usage.js +200 -45
- package/package.json +2 -1
package/dist/usage.js
CHANGED
|
@@ -5,63 +5,218 @@ export function turnEndedCounter(te, key) {
|
|
|
5
5
|
? Math.trunc(value)
|
|
6
6
|
: 0;
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
* Map request-local Cursor TurnEnded counters to AI SDK V3 usage (Kilo/OpenCode
|
|
10
|
-
* flatten via input total = noCache + cacheRead + cacheWrite). Only use this
|
|
11
|
-
* for a Run with no prior tool boundary; multi-step TurnEnded is cumulative.
|
|
12
|
-
*
|
|
13
|
-
* Contract: `input_tokens` is non-cached input; cache fields are separate.
|
|
14
|
-
* Cursor's agent wire currently sends `cache_write=0` even when later turns
|
|
15
|
-
* report large `cache_read`; we still forward the counter when non-zero.
|
|
16
|
-
* If production semantics differ (input_tokens already total), adjust here.
|
|
17
|
-
*/
|
|
18
|
-
export function buildLanguageModelV3UsageFromTurnEnded(te) {
|
|
19
|
-
const input_tokens = turnEndedCounter(te, "input_tokens");
|
|
20
|
-
const output_tokens = turnEndedCounter(te, "output_tokens");
|
|
21
|
-
const cache_read = turnEndedCounter(te, "cache_read");
|
|
22
|
-
const cache_write = turnEndedCounter(te, "cache_write");
|
|
23
|
-
const reasoning_tokens = turnEndedCounter(te, "reasoning_tokens");
|
|
8
|
+
export function cursorUsageCountersFromTurnEnded(te) {
|
|
24
9
|
return {
|
|
25
|
-
inputTokens:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
},
|
|
31
|
-
outputTokens: {
|
|
32
|
-
total: output_tokens + reasoning_tokens,
|
|
33
|
-
text: output_tokens,
|
|
34
|
-
reasoning: reasoning_tokens,
|
|
35
|
-
},
|
|
10
|
+
inputTokens: turnEndedCounter(te, "input_tokens"),
|
|
11
|
+
outputTokens: turnEndedCounter(te, "output_tokens"),
|
|
12
|
+
cacheRead: turnEndedCounter(te, "cache_read"),
|
|
13
|
+
cacheWrite: turnEndedCounter(te, "cache_write"),
|
|
14
|
+
reasoningTokens: turnEndedCounter(te, "reasoning_tokens"),
|
|
36
15
|
};
|
|
37
16
|
}
|
|
38
17
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* `est.inputTokens` tracks the seed plus delivered tool results, while
|
|
42
|
-
* `est.outputTokens` accumulates model output across earlier tool boundaries.
|
|
43
|
-
* Move prior output into the current input estimate and report only this
|
|
44
|
-
* doStream request's output. Cache counters are intentionally absent: the
|
|
45
|
-
* only counters Cursor exposes at TurnEnded are cumulative for the whole Run.
|
|
18
|
+
* Map Cursor counters to AI SDK V3. Cursor's `input_tokens` already includes
|
|
19
|
+
* cache reads/writes, and `output_tokens` already includes reasoning.
|
|
46
20
|
*/
|
|
47
|
-
export function
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
21
|
+
export function buildLanguageModelV3UsageFromCounters(counters, options = {}) {
|
|
22
|
+
const rawInput = Math.max(0, Math.trunc(counters.inputTokens));
|
|
23
|
+
const rawOutput = Math.max(0, Math.trunc(counters.outputTokens));
|
|
24
|
+
const rawCacheRead = Math.min(Math.max(0, Math.trunc(counters.cacheRead)), rawInput);
|
|
25
|
+
const rawCacheWrite = Math.min(Math.max(0, Math.trunc(counters.cacheWrite)), rawInput - rawCacheRead);
|
|
26
|
+
const rawReasoning = Math.min(Math.max(0, Math.trunc(counters.reasoningTokens)), rawOutput);
|
|
27
|
+
const contextTotal = options.contextTotalTokens;
|
|
28
|
+
const hasContextTotal = typeof contextTotal === "number" && Number.isFinite(contextTotal) && contextTotal >= 0;
|
|
29
|
+
const total = hasContextTotal ? Math.trunc(contextTotal) : rawInput + rawOutput;
|
|
30
|
+
const output = Math.min(rawOutput, total);
|
|
31
|
+
const input = Math.max(0, total - output);
|
|
32
|
+
// TurnEnded can aggregate several internal model calls, so its absolute input
|
|
33
|
+
// and cache counts can exceed the final checkpoint occupancy. Preserve the
|
|
34
|
+
// cache proportions while normalizing the partition to Cursor's context total.
|
|
35
|
+
const cacheRead = rawInput > 0
|
|
36
|
+
? Math.min(input, Math.round(input * rawCacheRead / rawInput))
|
|
37
|
+
: 0;
|
|
38
|
+
const cacheWrite = rawInput > 0
|
|
39
|
+
? Math.min(input - cacheRead, Math.round(input * rawCacheWrite / rawInput))
|
|
40
|
+
: 0;
|
|
41
|
+
const reasoning = rawOutput > 0
|
|
42
|
+
? Math.min(output, Math.round(output * rawReasoning / rawOutput))
|
|
43
|
+
: 0;
|
|
51
44
|
return {
|
|
52
45
|
inputTokens: {
|
|
53
|
-
total:
|
|
54
|
-
noCache:
|
|
55
|
-
cacheRead
|
|
56
|
-
cacheWrite
|
|
46
|
+
total: input,
|
|
47
|
+
noCache: Math.max(input - cacheRead - cacheWrite, 0),
|
|
48
|
+
cacheRead,
|
|
49
|
+
cacheWrite,
|
|
57
50
|
},
|
|
58
51
|
outputTokens: {
|
|
59
|
-
total:
|
|
60
|
-
text:
|
|
61
|
-
reasoning
|
|
52
|
+
total: output,
|
|
53
|
+
text: Math.max(output - reasoning, 0),
|
|
54
|
+
reasoning,
|
|
62
55
|
},
|
|
63
56
|
};
|
|
64
57
|
}
|
|
58
|
+
export function buildLanguageModelV3UsageFromTurnEnded(te, options = {}) {
|
|
59
|
+
return buildLanguageModelV3UsageFromCounters(cursorUsageCountersFromTurnEnded(te), options);
|
|
60
|
+
}
|
|
61
|
+
function usageCount(value) {
|
|
62
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
63
|
+
? Math.trunc(value)
|
|
64
|
+
: 0;
|
|
65
|
+
}
|
|
66
|
+
function usageRatio(part, total) {
|
|
67
|
+
return total > 0 ? `${(part / total * 100).toFixed(1)}%` : "n/a";
|
|
68
|
+
}
|
|
69
|
+
function categoryTokens(details) {
|
|
70
|
+
return new Map(details?.breakdown?.categories.map((category) => [
|
|
71
|
+
category.id || category.label || "(unnamed)",
|
|
72
|
+
category.estimatedTokens,
|
|
73
|
+
]) ?? []);
|
|
74
|
+
}
|
|
75
|
+
/** Compact, parseable category snapshot for checkpoint-by-checkpoint traces. */
|
|
76
|
+
export function formatCursorTokenCategories(details) {
|
|
77
|
+
const categories = Object.fromEntries(categoryTokens(details));
|
|
78
|
+
return Object.keys(categories).length > 0 ? JSON.stringify(categories) : "unavailable";
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Explain Cursor's aggregate cache counters using the state visible to this
|
|
82
|
+
* client. `stepStarts` is deliberately not called a model-call count: Cursor
|
|
83
|
+
* does not expose per-model-call cache accounting on the Run stream.
|
|
84
|
+
*/
|
|
85
|
+
export function formatCursorCacheDiagnostics(counters, current, prior, stats) {
|
|
86
|
+
const rawInput = Math.max(0, Math.trunc(counters.inputTokens));
|
|
87
|
+
const rawRead = Math.min(rawInput, Math.max(0, Math.trunc(counters.cacheRead)));
|
|
88
|
+
const rawWrite = Math.min(rawInput - rawRead, Math.max(0, Math.trunc(counters.cacheWrite)));
|
|
89
|
+
const rawUncached = Math.max(0, rawInput - rawRead - rawWrite);
|
|
90
|
+
const priorCategories = categoryTokens(prior);
|
|
91
|
+
const currentCategories = categoryTokens(current);
|
|
92
|
+
const categoriesComparable = !!prior?.breakdown && !!current?.breakdown;
|
|
93
|
+
const categoryDelta = {};
|
|
94
|
+
let sameSizedCategoryTokens = 0;
|
|
95
|
+
if (categoriesComparable) {
|
|
96
|
+
for (const id of new Set([...priorCategories.keys(), ...currentCategories.keys()])) {
|
|
97
|
+
const before = priorCategories.get(id);
|
|
98
|
+
const after = currentCategories.get(id);
|
|
99
|
+
if (before === undefined)
|
|
100
|
+
categoryDelta[id] = "new";
|
|
101
|
+
else if (after === undefined)
|
|
102
|
+
categoryDelta[id] = "removed";
|
|
103
|
+
else {
|
|
104
|
+
categoryDelta[id] = after - before;
|
|
105
|
+
if (after === before)
|
|
106
|
+
sameSizedCategoryTokens += after;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const continuity = stats.startedWithCheckpoint
|
|
111
|
+
? prior ? "warm" : "checkpoint-without-token-details"
|
|
112
|
+
: "cold";
|
|
113
|
+
const contextDelta = current && prior ? current.usedTokens - prior.usedTokens : undefined;
|
|
114
|
+
return [
|
|
115
|
+
"cache diagnosis:",
|
|
116
|
+
`sessionKey=${stats.sessionKey ?? "-"}`,
|
|
117
|
+
`conversationId=${stats.conversationId}`,
|
|
118
|
+
`conversationGroupId=${stats.conversationGroupId ?? "-"}`,
|
|
119
|
+
`model=${stats.modelId ?? "-"}`,
|
|
120
|
+
`continuity=${continuity}`,
|
|
121
|
+
`rawInput=${rawInput}`,
|
|
122
|
+
`rawCacheRead=${rawRead}`,
|
|
123
|
+
`rawCacheWrite=${rawWrite}`,
|
|
124
|
+
`rawUncached=${rawUncached}`,
|
|
125
|
+
`rawReadRatio=${usageRatio(rawRead, rawInput)}`,
|
|
126
|
+
`rawWriteRatio=${usageRatio(rawWrite, rawInput)}`,
|
|
127
|
+
`priorContext=${prior?.usedTokens ?? "unavailable"}`,
|
|
128
|
+
`currentContext=${current?.usedTokens ?? "unavailable"}`,
|
|
129
|
+
`contextDelta=${contextDelta ?? "unavailable"}`,
|
|
130
|
+
`rawReadVsPriorContext=${prior ? usageRatio(rawRead, prior.usedTokens) : "n/a"}`,
|
|
131
|
+
`sameSizedCategoryTokens=${categoriesComparable ? sameSizedCategoryTokens : "unavailable"}`,
|
|
132
|
+
`categoryDelta=${categoriesComparable && Object.keys(categoryDelta).length > 0 ? JSON.stringify(categoryDelta) : "unavailable"}`,
|
|
133
|
+
`requestContext=${stats.requestContextReused ? "reused" : "built"}`,
|
|
134
|
+
`requestContextHash=${stats.requestContextHash.slice(0, 16)}`,
|
|
135
|
+
`systemPromptHash=${stats.systemPromptHash?.slice(0, 16) ?? "none"}`,
|
|
136
|
+
`systemPromptSent=${!stats.startedWithCheckpoint}`,
|
|
137
|
+
`checkpointUpdates=${stats.checkpointUpdates}`,
|
|
138
|
+
`tokenDetailUpdates=${stats.tokenDetailUpdates}`,
|
|
139
|
+
`pumpPasses=${stats.pumpPasses}`,
|
|
140
|
+
`steps=${stats.stepStarts}/${stats.stepCompletes}`,
|
|
141
|
+
`displayToolCalls=${stats.displayToolCalls}`,
|
|
142
|
+
`execRequests=${stats.execRequests}`,
|
|
143
|
+
"perModelCallCache=unavailable",
|
|
144
|
+
].join(" ");
|
|
145
|
+
}
|
|
146
|
+
/** One-line proof that Cursor, AI SDK, and projected OpenCode totals agree. */
|
|
147
|
+
export function formatTurnUsageValidation(counters, usage, tokenDetails, contextSource) {
|
|
148
|
+
const input = usageCount(usage.inputTokens.total);
|
|
149
|
+
const noCache = usageCount(usage.inputTokens.noCache);
|
|
150
|
+
const cacheRead = usageCount(usage.inputTokens.cacheRead);
|
|
151
|
+
const cacheWrite = usageCount(usage.inputTokens.cacheWrite);
|
|
152
|
+
const inputParts = noCache + cacheRead + cacheWrite;
|
|
153
|
+
const output = usageCount(usage.outputTokens.total);
|
|
154
|
+
const text = usageCount(usage.outputTokens.text);
|
|
155
|
+
const reasoning = usageCount(usage.outputTokens.reasoning);
|
|
156
|
+
const outputParts = text + reasoning;
|
|
157
|
+
const sentTotal = input + output;
|
|
158
|
+
const projectedOpenCodeTotal = inputParts + outputParts;
|
|
159
|
+
const rawTotal = counters.inputTokens + counters.outputTokens;
|
|
160
|
+
const cursor = tokenDetails
|
|
161
|
+
? `${tokenDetails.usedTokens}/${tokenDetails.maxTokens}` +
|
|
162
|
+
`(${usageRatio(tokenDetails.usedTokens, tokenDetails.maxTokens)})`
|
|
163
|
+
: "unavailable";
|
|
164
|
+
const totalMatch = tokenDetails ? String(sentTotal === tokenDetails.usedTokens) : "unavailable";
|
|
165
|
+
const breakdown = tokenDetails?.breakdown;
|
|
166
|
+
const categorySum = breakdown?.categories.reduce((sum, category) => sum + category.estimatedTokens, 0);
|
|
167
|
+
const breakdownMatch = breakdown && categorySum !== undefined
|
|
168
|
+
? breakdown.totalUsedTokens === tokenDetails.usedTokens &&
|
|
169
|
+
categorySum === breakdown.totalUsedTokens
|
|
170
|
+
: undefined;
|
|
171
|
+
const rawCached = counters.cacheRead + counters.cacheWrite;
|
|
172
|
+
const sentCached = cacheRead + cacheWrite;
|
|
173
|
+
const cacheRatioMatch = tokenDetails
|
|
174
|
+
? counters.inputTokens > 0 && input > 0
|
|
175
|
+
? Math.abs(rawCached / counters.inputTokens - sentCached / input) <= 1 / input
|
|
176
|
+
: rawCached === 0 && sentCached === 0
|
|
177
|
+
: undefined;
|
|
178
|
+
const status = input === inputParts &&
|
|
179
|
+
output === outputParts &&
|
|
180
|
+
projectedOpenCodeTotal === sentTotal &&
|
|
181
|
+
(cacheRatioMatch ?? true) &&
|
|
182
|
+
(!tokenDetails || sentTotal === tokenDetails.usedTokens) &&
|
|
183
|
+
(breakdownMatch ?? true)
|
|
184
|
+
? "ok"
|
|
185
|
+
: "mismatch";
|
|
186
|
+
return [
|
|
187
|
+
"turn usage validation:",
|
|
188
|
+
`status=${status}`,
|
|
189
|
+
`source=${tokenDetails ? contextSource ?? "checkpoint-current-run" : "unavailable"}`,
|
|
190
|
+
`cursor=${cursor}`,
|
|
191
|
+
`rawTotal=${rawTotal}`,
|
|
192
|
+
`sentTotal=${sentTotal}`,
|
|
193
|
+
`totalMatch=${totalMatch}`,
|
|
194
|
+
`input=${input}`,
|
|
195
|
+
`inputParts=${inputParts}`,
|
|
196
|
+
`inputMatch=${input === inputParts}`,
|
|
197
|
+
`output=${output}`,
|
|
198
|
+
`outputParts=${outputParts}`,
|
|
199
|
+
`outputMatch=${output === outputParts}`,
|
|
200
|
+
`opencodeProjectedTotal=${projectedOpenCodeTotal}`,
|
|
201
|
+
`opencodeMatch=${projectedOpenCodeTotal === sentTotal}`,
|
|
202
|
+
`breakdownTotal=${breakdown?.totalUsedTokens ?? "unavailable"}`,
|
|
203
|
+
`categorySum=${categorySum ?? "unavailable"}`,
|
|
204
|
+
`breakdownMatch=${breakdownMatch ?? "unavailable"}`,
|
|
205
|
+
`rawCachedRatio=${usageRatio(rawCached, counters.inputTokens)}`,
|
|
206
|
+
`sentCachedRatio=${usageRatio(sentCached, input)}`,
|
|
207
|
+
`cacheRatioMatch=${cacheRatioMatch ?? "unavailable"}`,
|
|
208
|
+
].join(" ");
|
|
209
|
+
}
|
|
210
|
+
/** OpenCode requires a usage object at every step boundary. */
|
|
211
|
+
export function emptyLanguageModelV3Usage() {
|
|
212
|
+
return buildLanguageModelV3UsageFromCounters({
|
|
213
|
+
inputTokens: 0,
|
|
214
|
+
outputTokens: 0,
|
|
215
|
+
cacheRead: 0,
|
|
216
|
+
cacheWrite: 0,
|
|
217
|
+
reasoningTokens: 0,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
65
220
|
/** Mirror Kilo `Session.getUsage` / AI SDK flat fields from V3 nested usage. */
|
|
66
221
|
export function kiloShapedUsageFromV3(usage) {
|
|
67
222
|
const input = usage.inputTokens;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-opencode-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Use Cursor subscription models from OpenCode via Cursor's Connect-RPC agent protocol",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
"@opencode-ai/plugin": "^1.18.16",
|
|
93
93
|
"@opencode-ai/plugin-next": "npm:@opencode-ai/plugin@0.0.0-next-17155",
|
|
94
94
|
"@tsconfig/node22": "^22.0.5",
|
|
95
|
+
"@types/bun": "^1.3.0",
|
|
95
96
|
"@types/node": "^22.20.1",
|
|
96
97
|
"typescript": "^5.9.3"
|
|
97
98
|
}
|