cursor-opencode-provider 0.8.0 → 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.
Files changed (132) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/README.md +230 -44
  3. package/SECURITY.md +60 -12
  4. package/dist/activity.d.ts +73 -1
  5. package/dist/activity.js +231 -5
  6. package/dist/agent-url.js +37 -9
  7. package/dist/auth-renewal.d.ts +1 -1
  8. package/dist/auth-renewal.js +58 -26
  9. package/dist/billing.d.ts +105 -0
  10. package/dist/billing.js +212 -0
  11. package/dist/classic-tool-results.d.ts +1 -0
  12. package/dist/classic-tool-results.js +42 -0
  13. package/dist/context/build.d.ts +23 -4
  14. package/dist/context/build.js +65 -10
  15. package/dist/context/dynamic-catalog.js +6 -1
  16. package/dist/context/env.js +27 -3
  17. package/dist/context/epoch.js +9 -5
  18. package/dist/context/frozen.js +6 -2
  19. package/dist/context/git.d.ts +5 -2
  20. package/dist/context/git.js +17 -7
  21. package/dist/context/index.d.ts +2 -0
  22. package/dist/context/index.js +2 -0
  23. package/dist/context/paths.d.ts +45 -18
  24. package/dist/context/paths.js +72 -22
  25. package/dist/context/skills-bridge.d.ts +22 -0
  26. package/dist/context/skills-bridge.js +20 -0
  27. package/dist/context/skills.d.ts +34 -0
  28. package/dist/context/skills.js +211 -0
  29. package/dist/debug.js +13 -2
  30. package/dist/early-tool-rows.d.ts +60 -0
  31. package/dist/early-tool-rows.js +96 -0
  32. package/dist/errors.d.ts +49 -7
  33. package/dist/errors.js +163 -8
  34. package/dist/host-agent-mode.d.ts +53 -4
  35. package/dist/host-agent-mode.js +117 -13
  36. package/dist/host-interrupt.d.ts +12 -0
  37. package/dist/host-interrupt.js +18 -0
  38. package/dist/host-plan-file.d.ts +28 -0
  39. package/dist/host-plan-file.js +76 -0
  40. package/dist/host-steer.d.ts +71 -0
  41. package/dist/host-steer.js +232 -0
  42. package/dist/image-input.d.ts +18 -0
  43. package/dist/image-input.js +122 -30
  44. package/dist/image-save.d.ts +10 -2
  45. package/dist/image-save.js +33 -21
  46. package/dist/index.d.ts +2 -2
  47. package/dist/language-model.d.ts +149 -30
  48. package/dist/language-model.js +3127 -673
  49. package/dist/lost-request.d.ts +39 -0
  50. package/dist/lost-request.js +111 -0
  51. package/dist/model-config.d.ts +12 -1
  52. package/dist/model-config.js +78 -30
  53. package/dist/models.d.ts +26 -3
  54. package/dist/models.js +101 -45
  55. package/dist/opencode2/catalog.d.ts +1 -0
  56. package/dist/opencode2/catalog.js +2 -0
  57. package/dist/opencode2/image-save-tool.d.ts +8 -0
  58. package/dist/opencode2/image-save-tool.js +84 -0
  59. package/dist/opencode2/tool-results.d.ts +18 -0
  60. package/dist/opencode2/tool-results.js +66 -0
  61. package/dist/opencode2/types.d.ts +15 -0
  62. package/dist/parallel-step.d.ts +65 -0
  63. package/dist/parallel-step.js +102 -0
  64. package/dist/plugin-opencode2.js +233 -65
  65. package/dist/plugin.js +97 -23
  66. package/dist/pricing-data.d.ts +40 -3
  67. package/dist/pricing-data.js +40 -3
  68. package/dist/pricing.d.ts +1 -1
  69. package/dist/pricing.js +7 -4
  70. package/dist/process-shared.d.ts +14 -0
  71. package/dist/process-shared.js +18 -0
  72. package/dist/protocol/apply-patch.d.ts +25 -13
  73. package/dist/protocol/apply-patch.js +79 -30
  74. package/dist/protocol/ask-question.d.ts +12 -1
  75. package/dist/protocol/ask-question.js +77 -3
  76. package/dist/protocol/blob-store.d.ts +5 -1
  77. package/dist/protocol/blob-store.js +10 -6
  78. package/dist/protocol/client-version.js +6 -4
  79. package/dist/protocol/conversation-cache-header.d.ts +23 -0
  80. package/dist/protocol/conversation-cache-header.js +149 -0
  81. package/dist/protocol/conversation-persistence.d.ts +27 -1
  82. package/dist/protocol/conversation-persistence.js +628 -126
  83. package/dist/protocol/conversation-state.d.ts +8 -1
  84. package/dist/protocol/conversation-state.js +28 -7
  85. package/dist/protocol/create-plan.d.ts +47 -25
  86. package/dist/protocol/create-plan.js +33 -23
  87. package/dist/protocol/host-notes.d.ts +31 -0
  88. package/dist/protocol/host-notes.js +85 -0
  89. package/dist/protocol/interactions.d.ts +26 -17
  90. package/dist/protocol/interactions.js +80 -31
  91. package/dist/protocol/messages.js +164 -21
  92. package/dist/protocol/request.d.ts +40 -19
  93. package/dist/protocol/request.js +55 -28
  94. package/dist/protocol/sent-history-images.d.ts +49 -0
  95. package/dist/protocol/sent-history-images.js +102 -0
  96. package/dist/protocol/switch-mode.d.ts +66 -27
  97. package/dist/protocol/switch-mode.js +147 -73
  98. package/dist/protocol/token-details.d.ts +2 -0
  99. package/dist/protocol/token-details.js +11 -1
  100. package/dist/protocol/tool-call-bridge.d.ts +6 -0
  101. package/dist/protocol/tool-call-bridge.js +24 -0
  102. package/dist/protocol/tools.d.ts +90 -9
  103. package/dist/protocol/tools.js +543 -151
  104. package/dist/protocol/turn-provenance.d.ts +19 -4
  105. package/dist/protocol/turn-provenance.js +77 -20
  106. package/dist/replay-safety.d.ts +22 -4
  107. package/dist/replay-safety.js +86 -27
  108. package/dist/retry-budget.d.ts +21 -0
  109. package/dist/retry-budget.js +94 -0
  110. package/dist/runtime-files.d.ts +23 -0
  111. package/dist/runtime-files.js +581 -0
  112. package/dist/session.d.ts +129 -10
  113. package/dist/session.js +198 -35
  114. package/dist/shared.d.ts +1 -1
  115. package/dist/shared.js +1 -1
  116. package/dist/shell-timeout.d.ts +54 -10
  117. package/dist/shell-timeout.js +362 -92
  118. package/dist/stream-write.d.ts +20 -0
  119. package/dist/stream-write.js +87 -0
  120. package/dist/terminal-runner.d.ts +15 -0
  121. package/dist/terminal-runner.js +528 -0
  122. package/dist/todo-store.d.ts +2 -0
  123. package/dist/todo-store.js +19 -0
  124. package/dist/transport/connect.d.ts +10 -1
  125. package/dist/transport/connect.js +124 -27
  126. package/dist/usage.d.ts +37 -17
  127. package/dist/usage.js +85 -34
  128. package/package.json +5 -3
  129. package/dist/plan-execution-kickoff.d.ts +0 -57
  130. package/dist/plan-execution-kickoff.js +0 -186
  131. package/dist/protocol/thinking.d.ts +0 -15
  132. package/dist/protocol/thinking.js +0 -17
@@ -0,0 +1,39 @@
1
+ export type LostRequest = {
2
+ conversationId: string;
3
+ checkpointKey: string;
4
+ /** Everything Cursor has not checkpointed, as the last attempt sent it. */
5
+ text: string;
6
+ /** That attempt's own live text, before anything was prepended. */
7
+ latest: string;
8
+ };
9
+ export declare function checkpointKey(bytes: Uint8Array | undefined): string;
10
+ /**
11
+ * Prepend a user message whose Run never received a newer checkpoint.
12
+ * A history rebuild already contains it, so that path drops the record.
13
+ * A retry is the previous attempt's live text again: it sends what that
14
+ * attempt sent. A host that sends the same note before every attempt must not
15
+ * pile it up. Any other text is a new request and is appended, even when it
16
+ * repeats an older one (red, blue, then red again).
17
+ */
18
+ export declare function carryLostRequest(input: {
19
+ sessionKey: string;
20
+ conversationId: string;
21
+ checkpoint: Uint8Array | undefined;
22
+ text: string;
23
+ rebuildFromHistory: boolean;
24
+ }): string;
25
+ /** `text` is what this turn sends; `latest` is its own live text (defaults to `text`). */
26
+ export declare function rememberLostRequest(input: {
27
+ sessionKey: string;
28
+ conversationId: string;
29
+ checkpoint: Uint8Array | undefined;
30
+ text: string;
31
+ latest?: string;
32
+ }): void;
33
+ /** A checkpoint other than the one this turn was sent on means Cursor kept the turn. */
34
+ export declare function noteCheckpoint(sessionKey: string | undefined, bytes: Uint8Array): void;
35
+ export declare function hasLostRequest(sessionKey: string | undefined): boolean;
36
+ export declare function dropLostRequest(sessionKey: string | undefined): void;
37
+ export declare function serializeLostRequest(sessionKey: string | undefined): string | undefined;
38
+ export declare function restoreLostRequest(sessionKey: string, raw: string | undefined): void;
39
+ export declare function resetLostRequestsForTests(): void;
@@ -0,0 +1,111 @@
1
+ import { createHash } from "node:crypto";
2
+ import { processShared } from "./process-shared.js";
3
+ const MAX_RECORDS = 1_024;
4
+ const records = () => processShared("lost-request.v2", () => new Map());
5
+ export function checkpointKey(bytes) {
6
+ if (!bytes?.length)
7
+ return "none";
8
+ return createHash("sha256").update(bytes).digest("hex");
9
+ }
10
+ /**
11
+ * Prepend a user message whose Run never received a newer checkpoint.
12
+ * A history rebuild already contains it, so that path drops the record.
13
+ * A retry is the previous attempt's live text again: it sends what that
14
+ * attempt sent. A host that sends the same note before every attempt must not
15
+ * pile it up. Any other text is a new request and is appended, even when it
16
+ * repeats an older one (red, blue, then red again).
17
+ */
18
+ export function carryLostRequest(input) {
19
+ const map = records();
20
+ const record = map.get(input.sessionKey);
21
+ if (!record)
22
+ return input.text;
23
+ const key = checkpointKey(input.checkpoint);
24
+ if (input.rebuildFromHistory
25
+ || record.conversationId !== input.conversationId
26
+ || record.checkpointKey !== key) {
27
+ map.delete(input.sessionKey);
28
+ return input.text;
29
+ }
30
+ if (input.text === record.latest)
31
+ return record.text;
32
+ // The host already resends the lost text ahead of this one (OpenCode 1.x
33
+ // merges user messages left without a reply into one live turn).
34
+ if (input.text === record.text || input.text.startsWith(`${record.text}\n\n`))
35
+ return input.text;
36
+ map.delete(input.sessionKey);
37
+ return `${record.text}\n\n${input.text}`;
38
+ }
39
+ /** `text` is what this turn sends; `latest` is its own live text (defaults to `text`). */
40
+ export function rememberLostRequest(input) {
41
+ if (!input.sessionKey || !input.text || input.text === ".")
42
+ return;
43
+ setRecord(input.sessionKey, {
44
+ conversationId: input.conversationId,
45
+ checkpointKey: checkpointKey(input.checkpoint),
46
+ text: input.text,
47
+ latest: input.latest ?? input.text,
48
+ });
49
+ }
50
+ /** A session that never sends another turn must not keep its record forever. */
51
+ function setRecord(sessionKey, record) {
52
+ const map = records();
53
+ map.delete(sessionKey);
54
+ map.set(sessionKey, record);
55
+ while (map.size > MAX_RECORDS) {
56
+ const oldest = map.keys().next().value;
57
+ if (oldest === undefined)
58
+ break;
59
+ map.delete(oldest);
60
+ }
61
+ }
62
+ /** A checkpoint other than the one this turn was sent on means Cursor kept the turn. */
63
+ export function noteCheckpoint(sessionKey, bytes) {
64
+ if (!sessionKey)
65
+ return;
66
+ const map = records();
67
+ const record = map.get(sessionKey);
68
+ if (!record)
69
+ return;
70
+ if (checkpointKey(bytes) !== record.checkpointKey)
71
+ map.delete(sessionKey);
72
+ }
73
+ export function hasLostRequest(sessionKey) {
74
+ return !!sessionKey && records().has(sessionKey);
75
+ }
76
+ export function dropLostRequest(sessionKey) {
77
+ if (sessionKey)
78
+ records().delete(sessionKey);
79
+ }
80
+ export function serializeLostRequest(sessionKey) {
81
+ if (!sessionKey)
82
+ return undefined;
83
+ const record = records().get(sessionKey);
84
+ return record ? JSON.stringify(record) : undefined;
85
+ }
86
+ export function restoreLostRequest(sessionKey, raw) {
87
+ if (!raw || records().has(sessionKey))
88
+ return;
89
+ let parsed;
90
+ try {
91
+ parsed = JSON.parse(raw);
92
+ }
93
+ catch {
94
+ return;
95
+ }
96
+ if (typeof parsed.conversationId !== "string"
97
+ || typeof parsed.checkpointKey !== "string"
98
+ || typeof parsed.text !== "string"
99
+ || parsed.text.length === 0
100
+ || (parsed.latest !== undefined && typeof parsed.latest !== "string"))
101
+ return;
102
+ setRecord(sessionKey, {
103
+ conversationId: parsed.conversationId,
104
+ checkpointKey: parsed.checkpointKey,
105
+ text: parsed.text,
106
+ latest: parsed.latest ?? parsed.text,
107
+ });
108
+ }
109
+ export function resetLostRequestsForTests() {
110
+ records().clear();
111
+ }
@@ -1,4 +1,5 @@
1
1
  import { type ModelInfo, type ModelVariant } from "./models.js";
2
+ type ContextTier = "base" | "long" | number;
2
3
  /**
3
4
  * Display names shared by both a thinking and a non-thinking model. A thinking
4
5
  * model with such a name needs a "Thinking" tag to disambiguate it from its
@@ -8,10 +9,20 @@ import { type ModelInfo, type ModelVariant } from "./models.js";
8
9
  * tagged redundantly.
9
10
  */
10
11
  export declare function thinkingSuffixBaseNames(models: ModelInfo[]): Set<string>;
12
+ /**
13
+ * models.dev-style family for a Cursor model id: the id without its version
14
+ * segments and context/speed suffixes (`claude-haiku-4-5` → `claude-haiku`,
15
+ * `gemini-3.8-flash` → `gemini-flash`, `gpt-5.6-luna` → `gpt-luna`).
16
+ * Kimi keeps its major generation (`kimi-k2.7-code` → `kimi-k2`). OpenCode
17
+ * picks its small title model by family and falls back to the session model when
18
+ * no entry has one. Cursor Auto (`default`) is not a model family.
19
+ */
20
+ export declare function cursorModelFamily(id: string): string | undefined;
11
21
  export declare function modelInfoToConfig(mi: ModelInfo, options?: {
12
22
  thinkingSuffix?: boolean;
13
- contextTier?: "base" | "long";
23
+ contextTier?: ContextTier;
14
24
  variants?: ModelVariant[];
15
25
  fast?: boolean;
16
26
  }): Record<string, any>;
17
27
  export declare function modelsToConfig(models: ModelInfo[]): Record<string, any>;
28
+ export {};
@@ -1,4 +1,4 @@
1
- import { CURSOR_VARIANT_PARAMETERS_KEY, CURSOR_WIRE_MODEL_ID_KEY, parseCursorContextLimit, } from "./models.js";
1
+ import { CURSOR_VARIANT_PARAMETERS_KEY, CURSOR_WIRE_MODEL_ID_KEY, findDefaultVariant, parseCursorContextLimit, } from "./models.js";
2
2
  import { applyCursorModelCost, hasCursorFastPricing } from "./pricing.js";
3
3
  import { getDocumentedCursorModelContext, resolveCursorModelSupportsImages, } from "./model-metadata.js";
4
4
  /**
@@ -91,7 +91,7 @@ function modelInfoVariants(mi, variants) {
91
91
  // variant entry indistinguishable from the model entry in pickers that
92
92
  // collapse them.
93
93
  if (key === baseName && !usedKeys.has(key)) {
94
- key = `${baseName}${tagDims(v.parameterValues)}` || `${baseName} default`;
94
+ key = `${baseName}${tagDims(v.parameterValues) || " default"}`;
95
95
  }
96
96
  else if (usedKeys.has(key)) {
97
97
  key = `${sanitized}${tagDims(v.parameterValues)}`;
@@ -112,8 +112,23 @@ function isLongContextVariant(v) {
112
112
  function isFastVariant(v) {
113
113
  return v.parameterValues.some((p) => p.id === "fast" && p.value === "true");
114
114
  }
115
+ function variantContext(v) {
116
+ return parseCursorContextLimit(v.parameterValues.find((p) => p.id === "context")?.value);
117
+ }
118
+ function isSeparateContext(mi, v) {
119
+ const context = variantContext(v);
120
+ return isLongContextVariant(v) ||
121
+ (v.isMaxMode === true && context !== undefined && context !== mi.maxContext);
122
+ }
123
+ function contextSuffix(context) {
124
+ return context % 1_000_000 === 0 ? `${context / 1_000_000}m` : `${context / 1_000}k`;
125
+ }
115
126
  function variantsForTier(mi, tier) {
116
- return mi.variants.filter((v) => isLongContextVariant(v) === (tier === "long"));
127
+ return mi.variants.filter((v) => tier === "base"
128
+ ? !isSeparateContext(mi, v)
129
+ : tier === "long"
130
+ ? isLongContextVariant(v)
131
+ : isSeparateContext(mi, v) && variantContext(v) === tier);
117
132
  }
118
133
  /**
119
134
  * Display names shared by both a thinking and a non-thinking model. A thinking
@@ -140,6 +155,27 @@ export function thinkingSuffixBaseNames(models) {
140
155
  ambiguous.add(base);
141
156
  return ambiguous;
142
157
  }
158
+ /**
159
+ * models.dev-style family for a Cursor model id: the id without its version
160
+ * segments and context/speed suffixes (`claude-haiku-4-5` → `claude-haiku`,
161
+ * `gemini-3.8-flash` → `gemini-flash`, `gpt-5.6-luna` → `gpt-luna`).
162
+ * Kimi keeps its major generation (`kimi-k2.7-code` → `kimi-k2`). OpenCode
163
+ * picks its small title model by family and falls back to the session model when
164
+ * no entry has one. Cursor Auto (`default`) is not a model family.
165
+ */
166
+ export function cursorModelFamily(id) {
167
+ if (id === "default")
168
+ return undefined;
169
+ const base = id.replace(/(?:-(?:1m|fast))+$/, "");
170
+ const kimi = /^kimi-(k\d+)(?:[.p]\d+)*(?:-code(?:-highspeed)?)?$/.exec(base);
171
+ if (kimi)
172
+ return `kimi-${kimi[1]}`;
173
+ const family = base
174
+ .split("-")
175
+ .filter((part) => !/^\d+(?:[.p]\d+)*$/.test(part))
176
+ .join("-");
177
+ return family || undefined;
178
+ }
143
179
  export function modelInfoToConfig(mi, options = {}) {
144
180
  const contextTier = options.contextTier ?? "base";
145
181
  const variants = options.variants ?? variantsForTier(mi, contextTier);
@@ -150,6 +186,8 @@ export function modelInfoToConfig(mi, options = {}) {
150
186
  name += " Fast";
151
187
  if (contextTier === "long")
152
188
  name += " 1M";
189
+ if (typeof contextTier === "number")
190
+ name += ` ${contextSuffix(contextTier).toUpperCase()}`;
153
191
  // OpenCode's context limit is static per model entry, while Cursor's context
154
192
  // tier is a variant parameter. Long-context choices are therefore emitted as
155
193
  // separate OpenCode entries by modelsToConfig.
@@ -158,9 +196,12 @@ export function modelInfoToConfig(mi, options = {}) {
158
196
  // live checkpoints report 256k for the base tier — the previous 200k
159
197
  // fallback made the host context meter disagree with Cursor.
160
198
  const fallbackContext = mi.id === "default" ? 256_000 : 200_000;
161
- const context = contextTier === "long"
162
- ? (mi.maxContextForMaxMode ?? documentedContext?.maxContextForMaxMode ?? 1_000_000)
163
- : (mi.maxContext ?? documentedContext?.maxContext ?? fallbackContext);
199
+ const context = typeof contextTier === "number"
200
+ ? contextTier
201
+ : contextTier === "long"
202
+ ? (variants.some(isLongContextVariant) ? 1_000_000
203
+ : (mi.maxContextForMaxMode ?? documentedContext?.maxContextForMaxMode ?? 1_000_000))
204
+ : (mi.maxContext ?? documentedContext?.maxContext ?? fallbackContext);
164
205
  // OpenCode's overflow/compaction/UI use limit.context; generation and
165
206
  // thinking budgets use limit.output. models.dev 1M peers advertise
166
207
  // 64k–128k output — a tiny cap makes long-context sessions feel broken
@@ -182,13 +223,17 @@ export function modelInfoToConfig(mi, options = {}) {
182
223
  output,
183
224
  },
184
225
  };
226
+ // Optional cache metadata may supply a family; the live AvailableModels
227
+ // schema has no family field. Empty metadata must not block derivation.
228
+ const reported = typeof mi.family === "string" ? mi.family.trim() : "";
229
+ const family = reported || cursorModelFamily(mi.id);
230
+ if (family)
231
+ config.family = family;
185
232
  const variantConfig = modelInfoVariants(mi, variants);
186
233
  if (variantConfig)
187
234
  config.variants = variantConfig;
188
- if (contextTier === "long" || options.fast) {
189
- const defaultVariant = contextTier === "long"
190
- ? (variants.find((v) => v.isDefaultMax) ?? variants[0])
191
- : (variants.find((v) => v.isDefaultNonMax) ?? variants[0]);
235
+ if (contextTier !== "base" || options.fast) {
236
+ const defaultVariant = findDefaultVariant(mi, variants, contextTier !== "base") ?? variants[0];
192
237
  if (defaultVariant) {
193
238
  config.options = {
194
239
  [CURSOR_WIRE_MODEL_ID_KEY]: mi.id,
@@ -218,11 +263,9 @@ function uniqueCatalogId(usedIds, modelId, suffix) {
218
263
  }
219
264
  let n = 2;
220
265
  while (true) {
221
- const candidate = suffix === "-fast"
222
- ? `${modelId}-fast-${n}`
223
- : suffix === "-1m-fast"
224
- ? `${modelId}-1m-${n}-fast`
225
- : `${modelId}-1m-${n}`;
266
+ const candidate = suffix !== "-fast" && suffix.endsWith("-fast")
267
+ ? `${modelId}${suffix.slice(0, -5)}-${n}-fast`
268
+ : `${modelId}${suffix}-${n}`;
226
269
  if (!usedIds.has(candidate)) {
227
270
  usedIds.add(candidate);
228
271
  return candidate;
@@ -238,13 +281,28 @@ export function modelsToConfig(models) {
238
281
  const thinkingSuffix = !!m.supportsThinking && ambiguous.has(baseName(m));
239
282
  const splitFast = hasCursorFastPricing(m.id);
240
283
  const groups = [];
241
- for (const contextTier of ["base", "long"]) {
242
- const suffixFor = (fast) => contextTier === "long" ? (fast ? "-1m-fast" : "-1m") : (fast ? "-fast" : "");
243
- for (const group of speedGroups(variantsForTier(m, contextTier), splitFast)) {
284
+ const extraContexts = new Set(m.variants
285
+ .filter((v) => isSeparateContext(m, v) && !isLongContextVariant(v))
286
+ .map(variantContext)
287
+ .filter((context) => context !== undefined));
288
+ const tiers = ["base", "long", ...extraContexts];
289
+ for (const contextTier of tiers) {
290
+ const tierSuffix = contextTier === "base" ? ""
291
+ : contextTier === "long" ? "-1m" : `-${contextSuffix(contextTier)}`;
292
+ const suffixFor = (fast) => `${tierSuffix}${fast ? "-fast" : ""}`;
293
+ const tierVariants = new Set(variantsForTier(m, contextTier));
294
+ // Base/Fast ids and labels are persisted by the host. Keep their pre-split
295
+ // non-1M variants available while dedicated entries supply accurate limits.
296
+ const advertised = contextTier === "base"
297
+ ? m.variants.filter((variant) => !isLongContextVariant(variant))
298
+ : [...tierVariants];
299
+ for (const group of speedGroups(advertised, splitFast)) {
300
+ const tierGroup = group.variants.filter((variant) => tierVariants.has(variant));
244
301
  groups.push({
245
302
  contextTier,
246
303
  fast: group.fast,
247
- variants: group.variants,
304
+ variants: tierGroup.length > 0 ? tierGroup : group.variants,
305
+ advertisedVariants: group.variants,
248
306
  suffix: suffixFor(group.fast),
249
307
  });
250
308
  }
@@ -266,17 +324,7 @@ export function modelsToConfig(models) {
266
324
  variants: group.variants,
267
325
  fast: group.fast,
268
326
  });
269
- if (catalogId !== m.id && !config.options) {
270
- const defaultVariant = (group.contextTier === "long"
271
- ? group.variants.find((v) => v.isDefaultMax)
272
- : group.variants.find((v) => v.isDefaultNonMax)) ?? group.variants[0];
273
- if (defaultVariant) {
274
- config.options = {
275
- [CURSOR_WIRE_MODEL_ID_KEY]: m.id,
276
- [CURSOR_VARIANT_PARAMETERS_KEY]: defaultVariant.parameterValues.map((p) => ({ ...p })),
277
- };
278
- }
279
- }
327
+ config.variants = modelInfoVariants(m, group.advertisedVariants);
280
328
  out[catalogId] = applyCursorModelCost(pricingId, config);
281
329
  }
282
330
  }
package/dist/models.d.ts CHANGED
@@ -12,6 +12,11 @@ export type ModelVariant = {
12
12
  displayName: string;
13
13
  isDefaultNonMax: boolean;
14
14
  isDefaultMax: boolean;
15
+ /**
16
+ * Cursor's `AvailableModelVariant.is_max_mode`: this tuple runs only with
17
+ * request-level max mode. Absent when discovery omits the marker.
18
+ */
19
+ isMaxMode?: boolean;
15
20
  };
16
21
  export type ModelInfo = {
17
22
  id: string;
@@ -24,6 +29,11 @@ export type ModelInfo = {
24
29
  /** Context window when max-mode is on (proto field 16). */
25
30
  maxContextForMaxMode?: number;
26
31
  supportsMaxMode?: boolean;
32
+ /**
33
+ * `AvailableModel.supports_non_max_mode`; `false` means every variant runs
34
+ * in max mode. Absent when discovery omits it.
35
+ */
36
+ supportsNonMaxMode?: boolean;
27
37
  variants: ModelVariant[];
28
38
  };
29
39
  export type ModelCache = {
@@ -45,17 +55,30 @@ export declare function normalizeModelCache(value: unknown): ModelCache | null;
45
55
  */
46
56
  export declare function extractCursorVariantParameters(providerOptions: Record<string, unknown> | undefined): ModelParameterValue[] | undefined;
47
57
  export declare function resolveCursorWireModelId(providerOptions: Record<string, unknown> | undefined, fallback: string): string;
48
- /** True when resolved params select the long-context (1m) tier. */
58
+ /**
59
+ * True when resolved params select the long-context (1m) tier. Fallback for
60
+ * variants without Cursor's `isMaxMode` marker.
61
+ */
49
62
  export declare function paramsImplyMaxMode(params: ModelParameterValue[]): boolean;
50
63
  /**
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.
64
+ * Resolve the request-level max_mode bit. Like Cursor CLI, a cataloged variant
65
+ * requires max mode when Cursor marks it `isMaxMode` (Grok 4.7's 500k tier as
66
+ * well as 1m tiers) or the model does not support non-max mode. An explicit
67
+ * variant pick owns the context tier; otherwise a standalone maxMode hint may
68
+ * select max mode.
53
69
  */
54
70
  export declare function resolveVariantMaxMode(params: ModelParameterValue[], opts?: {
55
71
  picked?: ModelParameterValue[];
56
72
  maxMode?: boolean;
73
+ model?: ModelInfo;
57
74
  }): boolean;
58
75
  export declare function parseCursorContextLimit(value: unknown): number | undefined;
76
+ /**
77
+ * Find Cursor's declared default within a catalog subset, retaining its effort:
78
+ * the declared variant, else its speed twin, else its twin in another context
79
+ * tier (a 500k entry when the declared max default is 1m).
80
+ */
81
+ export declare function findDefaultVariant(model: ModelInfo, candidates: ModelVariant[], maxMode: boolean): ModelVariant | undefined;
59
82
  export declare function isCacheFresh(cache: ModelCache, ttlMs?: number): boolean;
60
83
  export declare function cacheFilePath(cacheDir: string): string;
61
84
  export declare function readCache(cacheDir: string): Promise<ModelCache | null>;