cursor-opencode-provider 0.6.2 → 0.6.4

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,
@@ -26,7 +26,7 @@ import { clearPersistedConversationState, hydrateConversationState, persistConve
26
26
  import { initializeConversationPersistence } from "./protocol/conversation-persistence.js";
27
27
  import { resolveContinuationPolicy, sessionManager, } from "./session.js";
28
28
  import { CursorAuthError, CursorLocalCancellationError, CursorProtocolError, CursorProviderError, CursorRetryExhaustedError, CursorServerError, CursorTransportError, isTransientGrpcStatus, retrySuppressedError, toCursorProviderError, } from "./errors.js";
29
- import { readCache, cacheFilePath, resolveVariantParameters, paramsImplyMaxMode, extractCursorVariantParameters, resolveCursorWireModelId } from "./models.js";
29
+ import { readCache, cacheFilePath, resolveVariantParameters, resolveVariantMaxMode, extractCursorVariantParameters, resolveCursorWireModelId } from "./models.js";
30
30
  import { getOrBuildRequestContext } from "./context/frozen.js";
31
31
  import { workspaceRootFromRequestContext } from "./context/env.js";
32
32
  import { ensureOpencodeProjectDir, opencodeGlobalCacheDir, setHostCacheDirOverride, } from "./context/paths.js";
@@ -760,9 +760,12 @@ async function startSession(modelId, token, callOptions, options, startOptions)
760
760
  maxMode: hintMaxMode,
761
761
  picked,
762
762
  });
763
- // Wire max_mode from the hint *or* a 1m context pick — OpenCode's variant
764
- // paramMap does not include a maxMode key when the user selects 1m.
765
- const maxMode = hintMaxMode || paramsImplyMaxMode(parameterValues);
763
+ // An explicit pick owns the context tier; otherwise honor the maxMode hint.
764
+ // OpenCode's variant paramMap does not include maxMode when the user selects 1m.
765
+ const maxMode = resolveVariantMaxMode(parameterValues, {
766
+ picked,
767
+ maxMode: hintMaxMode,
768
+ });
766
769
  // Do NOT pass callOptions.abortSignal into the h2 Run stream. OpenCode aborts
767
770
  // that signal when a turn ends with tool-calls; the Cursor stream must stay
768
771
  // open until we write the exec results on the next doStream.
package/dist/models.d.ts CHANGED
@@ -47,6 +47,14 @@ export declare function extractCursorVariantParameters(providerOptions: Record<s
47
47
  export declare function resolveCursorWireModelId(providerOptions: Record<string, unknown> | undefined, fallback: string): string;
48
48
  /** True when resolved params select the long-context (1m) tier. */
49
49
  export declare function paramsImplyMaxMode(params: ModelParameterValue[]): boolean;
50
+ /**
51
+ * Resolve the request-level max_mode bit. An explicit variant pick owns the
52
+ * context tier; otherwise a standalone maxMode hint may select max mode.
53
+ */
54
+ export declare function resolveVariantMaxMode(params: ModelParameterValue[], opts?: {
55
+ picked?: ModelParameterValue[];
56
+ maxMode?: boolean;
57
+ }): boolean;
50
58
  export declare function parseCursorContextLimit(value: unknown): number | undefined;
51
59
  export declare function isCacheFresh(cache: ModelCache, ttlMs?: number): boolean;
52
60
  export declare function cacheFilePath(cacheDir: string): string;
package/dist/models.js CHANGED
@@ -181,6 +181,13 @@ export function paramsImplyMaxMode(params) {
181
181
  return params.some((parameter) => parameter.id === "context" &&
182
182
  parseCursorContextLimit(parameter.value) === 1_000_000);
183
183
  }
184
+ /**
185
+ * Resolve the request-level max_mode bit. An explicit variant pick owns the
186
+ * context tier; otherwise a standalone maxMode hint may select max mode.
187
+ */
188
+ export function resolveVariantMaxMode(params, opts = {}) {
189
+ return paramsImplyMaxMode(params) || (opts.picked === undefined && opts.maxMode === true);
190
+ }
184
191
  // Cursor encodes a model's context window as a variant parameter `id: "context"`
185
192
  // whose value is a tier string — the same 200k / 272k / 300k / 1m the IDE's
186
193
  // picker shows. The base tier rides on the default non-max variant; the 1M
@@ -352,10 +359,7 @@ export function resolveVariantParameters(model, opts = {}) {
352
359
  pickedById.size === picked.length &&
353
360
  v.parameterValues.every((parameter) => pickedById.get(parameter.id) === parameter.value));
354
361
  if (exact) {
355
- return buildRequestedModelParams(exact.parameterValues, {
356
- reasoningEffort: opts.reasoningEffort,
357
- maxMode: wantMax,
358
- });
362
+ return buildRequestedModelParams(exact.parameterValues);
359
363
  }
360
364
  throw new CursorVariantSelectionError(`is stale for model ${JSON.stringify(model.id)}: its exact parameter tuple is unavailable`);
361
365
  }
@@ -171,15 +171,15 @@ export declare const CURSOR_MODEL_COSTS: {
171
171
  readonly cache_write: 0.25;
172
172
  };
173
173
  readonly "gpt-5.6-sol": {
174
- readonly input: 5;
175
- readonly output: 30;
176
- readonly cache_read: 0.5;
177
- readonly cache_write: 6.25;
174
+ readonly input: 4;
175
+ readonly output: 20;
176
+ readonly cache_read: 0.4;
177
+ readonly cache_write: 5;
178
178
  readonly context_over_200k: {
179
- readonly input: 10;
180
- readonly output: 30;
181
- readonly cache_read: 1;
182
- readonly cache_write: 12.5;
179
+ readonly input: 8;
180
+ readonly output: 20;
181
+ readonly cache_read: 0.8;
182
+ readonly cache_write: 10;
183
183
  };
184
184
  };
185
185
  readonly "gpt-5.6-terra": {
@@ -360,9 +360,6 @@ export declare const CURSOR_MODEL_CAPABILITIES: {
360
360
  readonly "composer-2.5": {
361
361
  readonly supportsImages: true;
362
362
  };
363
- readonly default: {
364
- readonly supportsImages: false;
365
- };
366
363
  readonly "gemini-2.5-flash": {
367
364
  readonly supportsImages: true;
368
365
  };
@@ -171,15 +171,15 @@ export const CURSOR_MODEL_COSTS = {
171
171
  "cache_write": 0.25
172
172
  },
173
173
  "gpt-5.6-sol": {
174
- "input": 5,
175
- "output": 30,
176
- "cache_read": 0.5,
177
- "cache_write": 6.25,
174
+ "input": 4,
175
+ "output": 20,
176
+ "cache_read": 0.4,
177
+ "cache_write": 5,
178
178
  "context_over_200k": {
179
- "input": 10,
180
- "output": 30,
181
- "cache_read": 1,
182
- "cache_write": 12.5
179
+ "input": 8,
180
+ "output": 20,
181
+ "cache_read": 0.8,
182
+ "cache_write": 10
183
183
  }
184
184
  },
185
185
  "gpt-5.6-terra": {
@@ -360,9 +360,6 @@ export const CURSOR_MODEL_CAPABILITIES = {
360
360
  "composer-2.5": {
361
361
  "supportsImages": true
362
362
  },
363
- "default": {
364
- "supportsImages": false
365
- },
366
363
  "gemini-2.5-flash": {
367
364
  "supportsImages": true
368
365
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-opencode-provider",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Use Cursor subscription models from OpenCode via Cursor's Connect-RPC agent protocol",
5
5
  "type": "module",
6
6
  "license": "MIT",