cursor-opencode-provider 0.6.1 → 0.6.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/README.md CHANGED
@@ -338,7 +338,7 @@ const model = cursor.languageModel("composer-2.5")
338
338
  // model implements AI SDK LanguageModelV3 (doStream / doGenerate)
339
339
  ```
340
340
 
341
- Pass either `accessToken` (JWT from OAuth or key exchange) or `apiKey` (raw `crsr_...` key). Optional: `apiBaseURL`, `agentBaseURL`, `cacheDir`, `headers`, `telemetryEnabled`, `retry`, and `continuation`. `cacheDir` pins the host cache root for model/version caches and Cursor project metadata; when omitted, the provider uses an injected structural host path bridge or the native OpenCode XDG cache described in [Paths](#paths-host-cache). Transient failures resume from the latest checkpoint produced by that Run, matching Cursor CLI; without an eligible checkpoint, retries remain limited to replay-safe attempts so completed text or tool work is not duplicated. Pending-tool inactivity is renewed by OpenCode activity from the session or its descendants. The older `baseURL` option is still accepted as a legacy alias for `agentBaseURL`.
341
+ Pass either `accessToken` (JWT from OAuth or key exchange) or `apiKey` (raw `crsr_...` key). Optional: `apiBaseURL`, `agentBaseURL`, `cacheDir`, `headers`, `telemetryEnabled`, `retry`, and `continuation`. `cacheDir` pins the host cache root for model/version caches and Cursor project metadata; when omitted, the provider uses an injected structural host path bridge or the native OpenCode XDG cache described in [Paths](#paths-host-cache). Transient failures resume from the latest checkpoint produced by that Run, matching Cursor CLI; without an eligible checkpoint, retries remain limited to replay-safe attempts so completed text or tool work is not duplicated. Once that budget is spent, or replay is unsafe, the terminal error does not re-arm OpenCode's outer retry loop. Pending-tool inactivity is renewed by OpenCode activity from the session or its descendants. The older `baseURL` option is still accepted as a legacy alias for `agentBaseURL`.
342
342
 
343
343
  ## Environment variables
344
344
 
package/dist/errors.d.ts CHANGED
@@ -43,6 +43,16 @@ export declare class CursorAuthError extends CursorProviderError {
43
43
  cause?: unknown;
44
44
  });
45
45
  }
46
+ /**
47
+ * OpenCode's SessionRetry treats several substrings in provider error messages
48
+ * as always-retryable — notably bare "unavailable" and "exhausted", plus a
49
+ * broader regex set (resource_exhausted, service unavailable, …).
50
+ *
51
+ * When this provider has already spent its Run retry budget, or has decided a
52
+ * replay would be unsafe, the host-facing message must not re-arm that loop.
53
+ * Keep the real gRPC/HTTP code on structured fields (`code`, `grpcStatus`).
54
+ */
55
+ export declare function sanitizeHostTerminalMessage(message: string): string;
46
56
  export declare class CursorRetryExhaustedError extends CursorProviderError {
47
57
  readonly attempts: number;
48
58
  constructor(attempts: number, last: CursorProviderError);
package/dist/errors.js CHANGED
@@ -69,10 +69,30 @@ export class CursorAuthError extends CursorProviderError {
69
69
  this.name = "CursorAuthError";
70
70
  }
71
71
  }
72
+ /**
73
+ * OpenCode's SessionRetry treats several substrings in provider error messages
74
+ * as always-retryable — notably bare "unavailable" and "exhausted", plus a
75
+ * broader regex set (resource_exhausted, service unavailable, …).
76
+ *
77
+ * When this provider has already spent its Run retry budget, or has decided a
78
+ * replay would be unsafe, the host-facing message must not re-arm that loop.
79
+ * Keep the real gRPC/HTTP code on structured fields (`code`, `grpcStatus`).
80
+ */
81
+ export function sanitizeHostTerminalMessage(message) {
82
+ return message
83
+ .replace(/\bresource[_\s-]?exhausted\b/gi, "capacity_limit")
84
+ .replace(/\bservice[_\s-]?unavailable\b/gi, "capacity_limit")
85
+ .replace(/\bunavailable\b/gi, "capacity_limit")
86
+ .replace(/\boverloaded\b/gi, "capacity_limit")
87
+ .replace(/\bretry exhausted\b/gi, "retries finished")
88
+ .replace(/\bexhausted\b/gi, "finished");
89
+ }
72
90
  export class CursorRetryExhaustedError extends CursorProviderError {
73
91
  attempts;
74
92
  constructor(attempts, last) {
75
- super(`Cursor retry exhausted after ${attempts} attempts: ${last.message}`, {
93
+ // Host-facing text must not contain OpenCode SessionRetry trigger words
94
+ // ("unavailable", "exhausted", …). Structured fields keep the real code.
95
+ super(`Cursor Run failed after ${attempts} attempts: ${sanitizeHostTerminalMessage(last.message)}`, {
76
96
  origin: last.origin,
77
97
  transient: false,
78
98
  replaySafe: false,
@@ -184,7 +204,7 @@ export function toCursorProviderError(error, options = { replaySafe: false }) {
184
204
  });
185
205
  }
186
206
  export function retrySuppressedError(cause, reason, attempt, maxAttempts) {
187
- const subject = cause.message.replace(/^Cursor Run stream /, "Cursor stream ");
207
+ const subject = sanitizeHostTerminalMessage(cause.message.replace(/^Cursor Run stream /, "Cursor stream "));
188
208
  return new CursorProviderError(`${subject} ${reason}; automatic retry unsafe (attempt ${attempt}/${maxAttempts})`, {
189
209
  origin: cause.origin,
190
210
  transient: false,
@@ -1707,7 +1707,10 @@ export async function pump(session, controller, ids, abortSignal) {
1707
1707
  const counters = te ? cursorUsageCountersFromTurnEnded(te) : undefined;
1708
1708
  const usage = settledUsage ?? (te
1709
1709
  ? tokenDetails
1710
- ? buildLanguageModelV3UsageFromCounters(counters, { contextTotalTokens: tokenDetails.usedTokens })
1710
+ ? buildLanguageModelV3UsageFromCounters(counters, {
1711
+ contextTotalTokens: tokenDetails.usedTokens,
1712
+ priorContextTokens: session.cacheDiagnostics?.priorTokenDetails?.usedTokens,
1713
+ })
1711
1714
  : emptyLanguageModelV3Usage()
1712
1715
  : emptyLanguageModelV3Usage());
1713
1716
  const providerMetadata = te
package/dist/usage.d.ts CHANGED
@@ -10,6 +10,8 @@ export type CursorUsageCounters = {
10
10
  export type CursorUsageOptions = {
11
11
  /** Cursor checkpoint occupancy, including the current turn's output. */
12
12
  contextTotalTokens?: number;
13
+ /** Previous turn's occupancy. When Cursor's cache read covers this window, do not dilute the hit by multi-step TurnEnded aggregates. */
14
+ priorContextTokens?: number;
13
15
  };
14
16
  export type CursorCacheDiagnosticStats = {
15
17
  sessionKey?: string;
package/dist/usage.js CHANGED
@@ -32,9 +32,17 @@ export function buildLanguageModelV3UsageFromCounters(counters, options = {}) {
32
32
  // TurnEnded can aggregate several internal model calls, so its absolute input
33
33
  // and cache counts can exceed the final checkpoint occupancy. Preserve the
34
34
  // cache proportions while normalizing the partition to Cursor's context total.
35
- const cacheRead = rawInput > 0
35
+ const proportionalRead = rawInput > 0
36
36
  ? Math.min(input, Math.round(input * rawCacheRead / rawInput))
37
37
  : 0;
38
+ const priorContext = options.priorContextTokens;
39
+ const prefixRead = typeof priorContext === "number"
40
+ && Number.isFinite(priorContext)
41
+ && priorContext > 0
42
+ && rawCacheRead >= priorContext
43
+ ? Math.min(input, Math.trunc(priorContext))
44
+ : 0;
45
+ const cacheRead = Math.min(input, Math.max(proportionalRead, prefixRead));
38
46
  const cacheWrite = rawInput > 0
39
47
  ? Math.min(input - cacheRead, Math.round(input * rawCacheWrite / rawInput))
40
48
  : 0;
@@ -170,9 +178,13 @@ export function formatTurnUsageValidation(counters, usage, tokenDetails, context
170
178
  : undefined;
171
179
  const rawCached = counters.cacheRead + counters.cacheWrite;
172
180
  const sentCached = cacheRead + cacheWrite;
181
+ const proportionalCached = counters.inputTokens > 0 && input > 0
182
+ ? Math.round(input * rawCached / counters.inputTokens)
183
+ : 0;
173
184
  const cacheRatioMatch = tokenDetails
174
185
  ? counters.inputTokens > 0 && input > 0
175
186
  ? Math.abs(rawCached / counters.inputTokens - sentCached / input) <= 1 / input
187
+ || (sentCached >= proportionalCached && sentCached <= input)
176
188
  : rawCached === 0 && sentCached === 0
177
189
  : undefined;
178
190
  const status = input === inputParts &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-opencode-provider",
3
- "version": "0.6.1",
3
+ "version": "0.6.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",