cursor-opencode-provider 0.4.3 → 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.
Files changed (37) hide show
  1. package/README.md +17 -12
  2. package/dist/context/build.d.ts +8 -0
  3. package/dist/context/build.js +72 -23
  4. package/dist/context/frozen.d.ts +29 -0
  5. package/dist/context/frozen.js +168 -0
  6. package/dist/context/index.d.ts +2 -1
  7. package/dist/context/index.js +2 -1
  8. package/dist/context/rules.d.ts +1 -0
  9. package/dist/context/rules.js +1 -1
  10. package/dist/language-model.d.ts +10 -9
  11. package/dist/language-model.js +387 -118
  12. package/dist/protocol/blob-reachability.d.ts +15 -0
  13. package/dist/protocol/blob-reachability.js +140 -0
  14. package/dist/protocol/blob-store.d.ts +37 -0
  15. package/dist/protocol/blob-store.js +115 -0
  16. package/dist/protocol/conversation-bind.d.ts +13 -0
  17. package/dist/protocol/conversation-bind.js +27 -1
  18. package/dist/protocol/conversation-persistence.d.ts +32 -0
  19. package/dist/protocol/conversation-persistence.js +504 -0
  20. package/dist/protocol/conversation-state.d.ts +16 -0
  21. package/dist/protocol/conversation-state.js +62 -0
  22. package/dist/protocol/kv.d.ts +1 -0
  23. package/dist/protocol/kv.js +2 -0
  24. package/dist/protocol/messages.d.ts +2 -0
  25. package/dist/protocol/messages.js +30 -2
  26. package/dist/protocol/request.d.ts +2 -0
  27. package/dist/protocol/request.js +2 -0
  28. package/dist/protocol/token-details.d.ts +33 -0
  29. package/dist/protocol/token-details.js +155 -0
  30. package/dist/protocol/tools.d.ts +17 -0
  31. package/dist/protocol/tools.js +76 -0
  32. package/dist/session.d.ts +39 -11
  33. package/dist/shared.d.ts +3 -0
  34. package/dist/shared.js +3 -0
  35. package/dist/usage.d.ts +40 -35
  36. package/dist/usage.js +199 -68
  37. package/package.json +1 -1
package/dist/usage.js CHANGED
@@ -5,86 +5,217 @@ export function turnEndedCounter(te, key) {
5
5
  ? Math.trunc(value)
6
6
  : 0;
7
7
  }
8
+ export function cursorUsageCountersFromTurnEnded(te) {
9
+ return {
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"),
15
+ };
16
+ }
8
17
  /**
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.
18
+ * Map Cursor counters to AI SDK V3. Cursor's `input_tokens` already includes
19
+ * cache reads/writes, and `output_tokens` already includes reasoning.
17
20
  */
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");
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;
24
44
  return {
25
45
  inputTokens: {
26
- total: input_tokens + cache_read + cache_write,
27
- noCache: input_tokens,
28
- cacheRead: cache_read,
29
- cacheWrite: cache_write,
46
+ total: input,
47
+ noCache: Math.max(input - cacheRead - cacheWrite, 0),
48
+ cacheRead,
49
+ cacheWrite,
30
50
  },
31
51
  outputTokens: {
32
- total: output_tokens + reasoning_tokens,
33
- text: output_tokens,
34
- reasoning: reasoning_tokens,
52
+ total: output,
53
+ text: Math.max(output - reasoning, 0),
54
+ reasoning,
35
55
  },
36
56
  };
37
57
  }
38
- /**
39
- * A single-step trust path for TurnEnded counters is only safe when the
40
- * totals are close to the request-local budget. Cursor reports cumulative
41
- * conversation/Run totals even on Runs that never crossed an OpenCode tool
42
- * boundary: single-step follow-up turns and parallel no-tools side calls
43
- * carry the whole conversation's `cache_read`. Treating those totals as
44
- * request-local inflates OpenCode's context usage and triggers premature
45
- * compaction (issue #20).
46
- */
47
- export const CUMULATIVE_TURN_ENDED_RATIO = 3;
48
- export const MIN_TRUSTED_TURN_ENDED_TOTAL = 1024;
49
- export function exceedsRequestLocalBudget(te, budget) {
50
- const total = turnEndedCounter(te, "input_tokens") +
51
- turnEndedCounter(te, "cache_read") +
52
- turnEndedCounter(te, "cache_write");
53
- const requestLocal = budget.inputTokens + budget.outputTokens;
54
- // Skip when the request-local seed is unknown; otherwise compare against
55
- // max(ratio × request-local, absolute floor) so tiny prompts still reject
56
- // multi-million-token conversation cache totals.
57
- if (requestLocal <= 0)
58
- return false;
59
- const trustCeiling = Math.max(requestLocal * CUMULATIVE_TURN_ENDED_RATIO, MIN_TRUSTED_TURN_ENDED_TOTAL);
60
- return total > trustCeiling;
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";
61
79
  }
62
80
  /**
63
- * Request-local usage for a held Cursor Run.
64
- *
65
- * `est.inputTokens` tracks the seed plus delivered tool results, while
66
- * `est.outputTokens` accumulates model output across earlier tool boundaries.
67
- * Move prior output into the current input estimate and report only this
68
- * doStream request's output. Cache counters are intentionally absent: the
69
- * only counters Cursor exposes at TurnEnded are cumulative for the whole Run.
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.
70
84
  */
71
- export function buildLanguageModelV3UsageFromEstimate(est, promptTokens, outputChars, estimateCharsToTokens) {
72
- const outputText = estimateCharsToTokens(outputChars);
73
- const priorOutput = Math.max(0, est.outputTokens - outputText);
74
- const inputNoCache = Math.max(promptTokens, est.inputTokens + priorOutput);
75
- return {
76
- inputTokens: {
77
- total: inputNoCache,
78
- noCache: inputNoCache,
79
- cacheRead: 0,
80
- cacheWrite: 0,
81
- },
82
- outputTokens: {
83
- total: outputText,
84
- text: outputText,
85
- reasoning: undefined,
86
- },
87
- };
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
+ });
88
219
  }
89
220
  /** Mirror Kilo `Session.getUsage` / AI SDK flat fields from V3 nested usage. */
90
221
  export function kiloShapedUsageFromV3(usage) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-opencode-provider",
3
- "version": "0.4.3",
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",