@bitkyc08/opencodex 2.39.0 → 2.40.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 (144) hide show
  1. package/README.md +49 -21
  2. package/gui/dist/assets/index-BHe2rl_C.js +112 -0
  3. package/gui/dist/assets/index-CJSb3HPe.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/cursor/catalog.ts +140 -7
  7. package/src/adapters/cursor/discovery.ts +79 -87
  8. package/src/adapters/cursor/effort-map.ts +11 -0
  9. package/src/adapters/cursor/request-builder.ts +31 -4
  10. package/src/adapters/cursor.ts +17 -0
  11. package/src/adapters/openai-chat.ts +95 -9
  12. package/src/adapters/openai-responses.ts +73 -9
  13. package/src/bridge.ts +58 -13
  14. package/src/claude/auth-mode.ts +6 -9
  15. package/src/claude/gateway-cache.ts +23 -3
  16. package/src/claude/model-info.ts +13 -2
  17. package/src/cli/access.ts +30 -0
  18. package/src/cli/account-extended.ts +19 -10
  19. package/src/cli/capabilities.ts +40 -1
  20. package/src/cli/claude-agent-startup-sync.ts +3 -0
  21. package/src/cli/claude.ts +134 -36
  22. package/src/cli/connect.ts +226 -0
  23. package/src/cli/dispatch.ts +126 -21
  24. package/src/cli/gui-pair-client.ts +170 -0
  25. package/src/cli/gui.ts +87 -0
  26. package/src/cli/help.ts +4 -1
  27. package/src/cli/index.ts +76 -22
  28. package/src/cli/models-runtime-subcommands.ts +34 -0
  29. package/src/cli/models-runtime.ts +4 -0
  30. package/src/cli/models.ts +2 -1
  31. package/src/cli/opencode.ts +2 -1
  32. package/src/cli/provider-runtime.ts +31 -1
  33. package/src/cli/registry.ts +32 -2
  34. package/src/cli/runtime-api.ts +10 -1
  35. package/src/cli/status.ts +26 -0
  36. package/src/cli/system-command.ts +9 -3
  37. package/src/client/connect.ts +650 -0
  38. package/src/client/hub-client.ts +457 -0
  39. package/src/client/hub-relay.ts +288 -0
  40. package/src/client/machine-api.ts +139 -0
  41. package/src/client/machine-auth.ts +54 -0
  42. package/src/client/machine-listener.ts +143 -0
  43. package/src/client/runtime.ts +93 -0
  44. package/src/client/state.ts +175 -0
  45. package/src/clients/config-export.ts +16 -5
  46. package/src/codex/auth-api.ts +169 -15
  47. package/src/codex/auth-context.ts +61 -6
  48. package/src/codex/catalog/aggregation.ts +17 -13
  49. package/src/codex/catalog/metadata.ts +28 -0
  50. package/src/codex/catalog/provider-fetch.ts +38 -5
  51. package/src/codex/catalog.ts +1 -1
  52. package/src/codex/desired-state.ts +13 -3
  53. package/src/codex/inject.ts +212 -31
  54. package/src/codex/journal.ts +54 -6
  55. package/src/codex/main-account.ts +105 -33
  56. package/src/codex/native-main-claim.ts +23 -1
  57. package/src/codex/native-residue.ts +7 -0
  58. package/src/codex/reset-credit-auto-redeem.ts +237 -0
  59. package/src/combos/request.ts +15 -3
  60. package/src/combos/types.ts +12 -0
  61. package/src/config/paths.ts +15 -1
  62. package/src/config/provider-validation.ts +40 -0
  63. package/src/config.ts +447 -3
  64. package/src/generated/compatibility-version.json +224 -108
  65. package/src/images/artifacts.ts +73 -37
  66. package/src/images/fulfill.ts +6 -1
  67. package/src/images/index.ts +1 -1
  68. package/src/images/plan.ts +16 -2
  69. package/src/images/synthetic-tool.ts +5 -0
  70. package/src/images/xai-client.ts +34 -1
  71. package/src/integrations/cursor-detect.ts +133 -0
  72. package/src/integrations/cursor-seen.ts +31 -0
  73. package/src/lib/errors.ts +18 -0
  74. package/src/lib/gui-pair-capability.ts +104 -0
  75. package/src/lib/lab-live-route-production.ts +2 -1
  76. package/src/lib/process-control.ts +4 -3
  77. package/src/lib/service-secrets.ts +161 -1
  78. package/src/lib/windows-system-proxy.ts +115 -0
  79. package/src/oauth/index.ts +3 -2
  80. package/src/oauth/pool-settings-capability.ts +55 -0
  81. package/src/providers/api-keys.ts +3 -1
  82. package/src/providers/codex-capacity.ts +34 -4
  83. package/src/providers/derive.ts +10 -0
  84. package/src/providers/fastwire.ts +10 -2
  85. package/src/providers/key-store.ts +197 -0
  86. package/src/providers/model-rename-migration.ts +3 -0
  87. package/src/providers/openai-sidecar.ts +3 -3
  88. package/src/providers/quota.ts +91 -42
  89. package/src/providers/registry.ts +33 -3
  90. package/src/reasoning-effort.ts +32 -0
  91. package/src/remote/protocol.ts +109 -0
  92. package/src/responses/citation-markers.ts +101 -0
  93. package/src/responses/parser.ts +43 -3
  94. package/src/responses/spill-store.ts +9 -4
  95. package/src/router.ts +72 -8
  96. package/src/server/auth-cors.ts +37 -1
  97. package/src/server/gui-session.ts +449 -0
  98. package/src/server/gui-static.ts +24 -7
  99. package/src/server/images.ts +237 -10
  100. package/src/server/index.ts +356 -30
  101. package/src/server/management/agent-settings-routes.ts +7 -1
  102. package/src/server/management/api-key-rotation.ts +74 -0
  103. package/src/server/management/combo-routes.ts +18 -6
  104. package/src/server/management/config-routes.ts +22 -3
  105. package/src/server/management/context.ts +5 -1
  106. package/src/server/management/cursor-integration-routes.ts +98 -0
  107. package/src/server/management/logs-usage-routes.ts +2 -1
  108. package/src/server/management/model-routes.ts +79 -1
  109. package/src/server/management/model-rows.ts +23 -1
  110. package/src/server/management/oauth-account-routes.ts +135 -2
  111. package/src/server/management/provider-routes.ts +143 -9
  112. package/src/server/management/route-registry.ts +10 -0
  113. package/src/server/management/session-routes.ts +13 -0
  114. package/src/server/management/system-routes.ts +13 -1
  115. package/src/server/management-api.ts +8 -3
  116. package/src/server/management-auth.ts +124 -82
  117. package/src/server/models-capabilities.ts +124 -0
  118. package/src/server/ports.ts +7 -0
  119. package/src/server/proxy-liveness.ts +7 -0
  120. package/src/server/request-log.ts +11 -5
  121. package/src/server/responses/agent-task-recovery.ts +1 -0
  122. package/src/server/responses/codex-auth-error.ts +3 -1
  123. package/src/server/responses/collaboration.ts +26 -1
  124. package/src/server/responses/combo-stream-preflight.ts +25 -6
  125. package/src/server/responses/compact.ts +32 -12
  126. package/src/server/responses/context-overflow.ts +49 -0
  127. package/src/server/responses/core.ts +196 -12
  128. package/src/server/responses/fetch-helpers.ts +2 -1
  129. package/src/server/responses/outbound-body-guard.ts +110 -0
  130. package/src/server/responses/ws-upstream.ts +87 -6
  131. package/src/server/responses-self-named-namespace-scrub.ts +181 -0
  132. package/src/server/system-env.ts +78 -25
  133. package/src/service.ts +218 -42
  134. package/src/types/config.ts +134 -0
  135. package/src/types/provider.ts +54 -1
  136. package/src/types/request.ts +2 -0
  137. package/src/types.ts +5 -0
  138. package/src/update/job.ts +33 -5
  139. package/src/usage/cost.ts +32 -1
  140. package/src/usage/expected-prices.ts +16 -0
  141. package/src/usage/log.ts +6 -2
  142. package/src/usage/summary.ts +48 -6
  143. package/gui/dist/assets/index-D-lchsPw.js +0 -112
  144. package/gui/dist/assets/index-uvENYLin.css +0 -1
@@ -32,6 +32,12 @@ export interface CursorCapability {
32
32
  readonly variants: Partial<Record<CursorVariantKind, CursorVariantSpec>>;
33
33
  /** Which variant the umbrella picker row selects (thinking merges into the base). */
34
34
  readonly defaultVariant: CursorVariantKind;
35
+ /**
36
+ * Human picker label, in Cursor's own spelling. Codex would otherwise show the raw
37
+ * routed slug (`cursor/kimi-k3`), because `routedDisplayName` passes it through
38
+ * unchanged for every provider (codex/catalog/sync.ts).
39
+ */
40
+ readonly displayName: string;
35
41
  /** Context-window metadata (display/routing only — never implies maxMode). */
36
42
  readonly window: number;
37
43
  /** Max Mode proven on the wire for this base (static evidence; live maxModeModels unions in). */
@@ -46,6 +52,8 @@ const CONTEXT_256K = 256 * K;
46
52
  const CONTEXT_272K = 272 * K;
47
53
  const CONTEXT_500K = 500 * K;
48
54
  const CONTEXT_1M = 1_000 * K;
55
+ /** Gemini publishes the exact power-of-two window, not a rounded 1M. */
56
+ const CONTEXT_GEMINI = 1_048_576;
49
57
 
50
58
  const FULL = ["low", "medium", "high", "xhigh", "max"] as const;
51
59
  const T = "thinking-then-effort" as const;
@@ -59,6 +67,7 @@ const E = "effort-then-thinking" as const;
59
67
  */
60
68
  export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
61
69
  "claude-4.5-opus": {
70
+ displayName: "Claude Opus 4.5",
62
71
  window: CONTEXT_200K,
63
72
  defaultVariant: "thinking",
64
73
  variants: {
@@ -67,6 +76,7 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
67
76
  },
68
77
  },
69
78
  "claude-4.6-opus": {
79
+ displayName: "Claude Opus 4.6",
70
80
  window: CONTEXT_1M,
71
81
  defaultVariant: "thinking",
72
82
  variants: {
@@ -75,6 +85,7 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
75
85
  },
76
86
  },
77
87
  "claude-4.6-sonnet": {
88
+ displayName: "Claude Sonnet 4.6",
78
89
  window: CONTEXT_1M,
79
90
  defaultVariant: "thinking",
80
91
  variants: {
@@ -83,6 +94,7 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
83
94
  },
84
95
  },
85
96
  "claude-4.5-sonnet": {
97
+ displayName: "Claude Sonnet 4.5",
86
98
  window: CONTEXT_200K,
87
99
  defaultVariant: "thinking",
88
100
  variants: {
@@ -91,6 +103,7 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
91
103
  },
92
104
  },
93
105
  "claude-4-sonnet": {
106
+ displayName: "Claude Sonnet 4",
94
107
  window: CONTEXT_200K,
95
108
  defaultVariant: "thinking",
96
109
  variants: {
@@ -99,6 +112,7 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
99
112
  },
100
113
  },
101
114
  "claude-fable-5": {
115
+ displayName: "Claude Fable 5",
102
116
  window: CONTEXT_1M,
103
117
  defaultVariant: "thinking",
104
118
  variants: {
@@ -106,7 +120,41 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
106
120
  thinking: { levels: FULL, order: T },
107
121
  },
108
122
  },
123
+ // 260902 preemptive: Claude Fable 5.1 seeded ahead of Cursor's lineup update, mirroring
124
+ // claude-fable-5 (same 1M window and full effort ladder). Cursor has spelled Claude ids
125
+ // both Anthropic-style (`claude-opus-4-7`, thinking-then-effort) and version-first
126
+ // (`claude-4.6-opus`, effort-then-thinking), so all three plausible spellings are seeded;
127
+ // the live GetUsableModels filter drops whichever the roster does not expose. Collapse to
128
+ // the one real spelling once it is observed.
129
+ "claude-fable-5-1": {
130
+ displayName: "Claude Fable 5.1",
131
+ window: CONTEXT_1M,
132
+ defaultVariant: "thinking",
133
+ variants: {
134
+ regular: { levels: FULL },
135
+ thinking: { levels: FULL, order: T },
136
+ },
137
+ },
138
+ "claude-fable-5.1": {
139
+ displayName: "Claude Fable 5.1",
140
+ window: CONTEXT_1M,
141
+ defaultVariant: "thinking",
142
+ variants: {
143
+ regular: { levels: FULL },
144
+ thinking: { levels: FULL, order: T },
145
+ },
146
+ },
147
+ "claude-5.1-fable": {
148
+ displayName: "Claude Fable 5.1",
149
+ window: CONTEXT_1M,
150
+ defaultVariant: "thinking",
151
+ variants: {
152
+ regular: { levels: FULL },
153
+ thinking: { levels: FULL, order: E },
154
+ },
155
+ },
109
156
  "claude-sonnet-5": {
157
+ displayName: "Claude Sonnet 5",
110
158
  window: CONTEXT_1M,
111
159
  defaultVariant: "thinking",
112
160
  variants: {
@@ -115,6 +163,7 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
115
163
  },
116
164
  },
117
165
  "claude-opus-4-7": {
166
+ displayName: "Claude Opus 4.7",
118
167
  window: CONTEXT_1M,
119
168
  defaultVariant: "thinking",
120
169
  variants: {
@@ -125,6 +174,7 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
125
174
  },
126
175
  },
127
176
  "claude-opus-4-8": {
177
+ displayName: "Claude Opus 4.8",
128
178
  window: CONTEXT_1M,
129
179
  defaultVariant: "thinking",
130
180
  variants: {
@@ -135,6 +185,7 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
135
185
  },
136
186
  },
137
187
  "claude-opus-5": {
188
+ displayName: "Claude Opus 5",
138
189
  window: CONTEXT_1M,
139
190
  defaultVariant: "thinking",
140
191
  variants: {
@@ -147,32 +198,38 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
147
198
  },
148
199
  },
149
200
  "glm-5.2": {
201
+ displayName: "GLM 5.2",
150
202
  window: CONTEXT_1M,
151
203
  defaultVariant: "regular",
152
204
  variants: { regular: { levels: ["high", "max"] } },
153
205
  },
154
206
  "glm-5.3": {
207
+ displayName: "GLM 5.3",
155
208
  window: CONTEXT_1M,
156
209
  defaultVariant: "regular",
157
210
  variants: { regular: { levels: ["low", "high", "max"] } },
158
211
  },
159
212
  "gemini-3.6-flash": {
160
- window: CONTEXT_1M,
213
+ displayName: "Gemini 3.6 Flash",
214
+ window: CONTEXT_GEMINI,
161
215
  defaultVariant: "regular",
162
216
  variants: { regular: { levels: ["minimal", "low", "medium", "high"] } },
163
217
  },
164
218
  "gemini-3.7-flash": {
165
- window: CONTEXT_1M,
219
+ displayName: "Gemini 3.7 Flash",
220
+ window: CONTEXT_GEMINI,
166
221
  defaultVariant: "regular",
167
222
  variants: { regular: { levels: ["low", "medium", "high"] } },
168
223
  },
169
224
  "kimi-k3": {
225
+ displayName: "Kimi K3",
170
226
  window: CONTEXT_1M,
171
227
  defaultVariant: "regular",
172
228
  maxModeVerified: true,
173
229
  variants: { regular: { levels: ["low", "high", "max"] } },
174
230
  },
175
231
  "grok-4.5": {
232
+ displayName: "Cursor Grok 4.5",
176
233
  window: CONTEXT_500K,
177
234
  defaultVariant: "regular",
178
235
  wirePrefix: "cursor-",
@@ -182,6 +239,7 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
182
239
  },
183
240
  },
184
241
  "grok-4.6": {
242
+ displayName: "Cursor Grok 4.6",
185
243
  window: CONTEXT_500K,
186
244
  defaultVariant: "regular",
187
245
  wirePrefix: "cursor-",
@@ -191,71 +249,88 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
191
249
  },
192
250
  },
193
251
  "gpt-5.1": {
252
+ displayName: "GPT-5.1",
194
253
  window: CONTEXT_272K,
195
254
  defaultVariant: "regular",
196
255
  variants: { regular: { levels: ["low", "high"] } },
197
256
  },
198
257
  "gpt-5.1-codex-max": {
258
+ displayName: "GPT-5.1 Codex Max",
199
259
  window: CONTEXT_272K,
200
260
  defaultVariant: "regular",
201
261
  variants: { regular: { levels: ["low", "medium", "high", "xhigh"] } },
202
262
  },
203
263
  "gpt-5.1-codex-mini": {
264
+ displayName: "GPT-5.1 Codex Mini",
204
265
  window: CONTEXT_272K,
205
266
  defaultVariant: "regular",
206
267
  variants: { regular: { levels: ["low", "high"] } },
207
268
  },
208
269
  "gpt-5.2": {
270
+ displayName: "GPT-5.2",
209
271
  window: CONTEXT_272K,
210
272
  defaultVariant: "regular",
211
273
  variants: { regular: { levels: ["low", "high", "xhigh"] } },
212
274
  },
213
275
  "gpt-5.2-codex": {
276
+ displayName: "GPT-5.2 Codex",
214
277
  window: CONTEXT_272K,
215
278
  defaultVariant: "regular",
216
279
  variants: { regular: { levels: ["low", "high", "xhigh"] } },
217
280
  },
218
281
  "gpt-5.3-codex": {
282
+ displayName: "Codex 5.3",
219
283
  window: CONTEXT_272K,
220
284
  defaultVariant: "regular",
221
285
  variants: { regular: { levels: ["low", "high", "xhigh"] } },
222
286
  },
223
287
  "gpt-5.4": {
288
+ displayName: "GPT-5.4",
224
289
  window: CONTEXT_272K,
225
290
  defaultVariant: "regular",
226
291
  variants: { regular: { levels: ["low", "medium", "high", "xhigh"] } },
227
292
  },
228
293
  "gpt-5.4-mini": {
294
+ displayName: "GPT-5.4 Mini",
229
295
  window: CONTEXT_272K,
230
296
  defaultVariant: "regular",
231
297
  variants: { regular: { levels: ["low", "medium", "high", "xhigh"] } },
232
298
  },
233
299
  "gpt-5.4-nano": {
300
+ displayName: "GPT-5.4 Nano",
234
301
  window: CONTEXT_272K,
235
302
  defaultVariant: "regular",
236
303
  variants: { regular: { levels: ["low", "medium", "high", "xhigh"] } },
237
304
  },
238
305
  "gpt-5.5": {
306
+ displayName: "GPT-5.5",
239
307
  window: CONTEXT_272K,
240
308
  defaultVariant: "regular",
241
309
  variants: { regular: { levels: ["low", "medium", "high"] } },
242
310
  },
243
311
  "gpt-5.5-extra": {
244
- window: CONTEXT_272K,
312
+ displayName: "GPT-5.5 Extra",
313
+ // Live GetUsableModels reports 200K for this row, not the gpt-5 family's 272K
314
+ // (account-verified 260709). The seed carried the measured number; the capability
315
+ // table was approximating from the family.
316
+ window: CONTEXT_200K,
245
317
  defaultVariant: "regular",
246
318
  variants: { regular: { levels: ["high"] } },
247
319
  },
248
320
  "gpt-5.6-sol": {
321
+ displayName: "GPT-5.6 Sol",
249
322
  window: CONTEXT_1M,
250
323
  defaultVariant: "regular",
251
324
  variants: { regular: { levels: FULL } },
252
325
  },
253
326
  "gpt-5.6-terra": {
327
+ displayName: "GPT-5.6 Terra",
254
328
  window: CONTEXT_1M,
255
329
  defaultVariant: "regular",
256
330
  variants: { regular: { levels: FULL } },
257
331
  },
258
332
  "gpt-5.6-luna": {
333
+ displayName: "GPT-5.6 Luna",
259
334
  window: CONTEXT_1M,
260
335
  defaultVariant: "regular",
261
336
  variants: { regular: { levels: FULL } },
@@ -368,6 +443,53 @@ function defaultKindFor(baseId: string): CursorVariantKind {
368
443
  return CURSOR_CAPABILITIES[baseId]?.defaultVariant ?? "regular";
369
444
  }
370
445
 
446
+ /**
447
+ * Promote a variant to its fast sibling when the base declares one, else leave it alone.
448
+ *
449
+ * Thinking must map to thinkingFast rather than to the plain fast variant: the umbrella row
450
+ * for a Claude base routes THINKING, and its regular-fast sibling is a different product
451
+ * with a shorter ladder (claude-opus-5-fast stops at high) whose regular family is
452
+ * quarantined. A base with no fast dimension keeps its kind, so Fast degrades to today's
453
+ * behavior instead of erroring.
454
+ */
455
+ export function upgradeToFast(baseId: string, kind: CursorVariantKind): CursorVariantKind {
456
+ const variants = CURSOR_CAPABILITIES[baseId]?.variants;
457
+ if (!variants) return kind;
458
+ if (kind === "thinking" || kind === "thinkingFast") {
459
+ return variants.thinkingFast ? "thinkingFast" : kind;
460
+ }
461
+ return variants.fast ? "fast" : kind;
462
+ }
463
+
464
+ /** Bases whose capability declares a fast or thinking-fast variant. */
465
+ export function cursorFastCapableBases(): string[] {
466
+ return Object.entries(CURSOR_CAPABILITIES)
467
+ .filter(([, capability]) => capability.variants.fast !== undefined
468
+ || capability.variants.thinkingFast !== undefined)
469
+ .map(([baseId]) => baseId);
470
+ }
471
+
472
+ /**
473
+ * The id to LIST for a base when the global fast switch is on, for clients that have no
474
+ * Fast toggle of their own. Undefined when the base has no fast dimension, so a caller
475
+ * cannot advertise an id that would not route.
476
+ *
477
+ * Composed from the base's defaultVariant rather than a bare `-fast` suffix. Measured: for a
478
+ * thinking-default base, `claude-opus-5-fast` parses back as the REGULAR-fast sibling and
479
+ * resolves to `claude-opus-5-high-fast` — a shorter ladder, in the quarantined regular
480
+ * family, and a different wire from what the Codex toggle sends. The mirror case is equally
481
+ * wrong: grok has no thinkingFast spec, so `grok-4.6-thinking-fast` would fall back to the
482
+ * regular spec and emit a bare `grok-4.6` with no effort and no fast marker at all.
483
+ */
484
+ export function cursorFastIdFor(baseId: string): string | undefined {
485
+ const capability = CURSOR_CAPABILITIES[baseId];
486
+ if (!capability) return undefined;
487
+ const kind = upgradeToFast(baseId, capability.defaultVariant);
488
+ if (kind === "thinkingFast") return `${baseId}-thinking-fast`;
489
+ if (kind === "fast") return `${baseId}-fast`;
490
+ return undefined;
491
+ }
492
+
371
493
  function normalizeRequestedEffort(reasoning: string | undefined): string | undefined {
372
494
  const normalized = reasoning?.toLowerCase();
373
495
  return normalized === "ultra" ? "max" : normalized;
@@ -448,20 +570,25 @@ export function resolveCursorSelection(
448
570
  pickedId: string,
449
571
  reasoning: string | undefined,
450
572
  liveMaxModeIds?: ReadonlySet<string>,
573
+ options: { fast?: boolean } = {},
451
574
  ): CursorResolvedSelection {
452
575
  const parsed = parseCursorVariantId(pickedId);
453
576
  if (!parsed.known) {
454
577
  return { wireId: pickedId, canonicalId: pickedId, maxMode: false, known: false };
455
578
  }
456
579
  const capability = CURSOR_CAPABILITIES[parsed.baseId]!;
457
- const spec = capability.variants[parsed.kind] ?? capability.variants.regular;
580
+ // Codex's Fast toggle is a variant switch here; every later read must use the upgraded
581
+ // kind, not parsed.kind, or the wire id loses its -fast marker (or keeps the cursor-
582
+ // prefix that only the regular variant takes).
583
+ const kind = options.fast === true ? upgradeToFast(parsed.baseId, parsed.kind) : parsed.kind;
584
+ const spec = capability.variants[kind] ?? capability.variants.regular;
458
585
  if (!spec) {
459
586
  return { wireId: parsed.baseId, canonicalId: parsed.baseId, maxMode: false, known: true };
460
587
  }
461
588
  const requested = parsed.level ?? reasoning;
462
589
  const effort = cursorVariantEffort(spec, requested);
463
- const canonicalId = composeWireId(parsed.baseId, parsed.kind, effort);
464
- const wireId = capability.wirePrefix && parsed.kind === "regular"
590
+ const canonicalId = composeWireId(parsed.baseId, kind, effort);
591
+ const wireId = capability.wirePrefix && kind === "regular"
465
592
  ? `${capability.wirePrefix}${canonicalId}`
466
593
  : canonicalId;
467
594
  const ultraRequested = parsed.ultra || reasoning?.toLowerCase() === "ultra";
@@ -493,6 +620,7 @@ export function liveCursorMaxModeBasesForTests(): ReadonlySet<string> {
493
620
 
494
621
  export interface CursorUmbrellaRow {
495
622
  readonly id: string;
623
+ readonly displayName: string;
496
624
  readonly efforts: readonly string[];
497
625
  readonly window: number;
498
626
  /** Max Mode evidence present: the ultra rung maps to maxMode on the wire. */
@@ -508,9 +636,13 @@ export interface CursorUmbrellaRow {
508
636
  export function cursorGrokFastSelection(
509
637
  pickedId: string,
510
638
  reasoning: string | undefined,
639
+ fast?: boolean,
511
640
  ): { wireBaseId: string; effort: string } | undefined {
512
641
  const parsed = parseCursorVariantId(pickedId);
513
- if (!parsed.known || parsed.kind !== "fast") return undefined;
642
+ // Both call paths must learn the flag together: if only resolveCursorSelection did, a
643
+ // toggled Grok pick would emit a flattened grok-4.6-high-fast id, which the wire rejects.
644
+ const kind = fast === true ? upgradeToFast(parsed.baseId, parsed.kind) : parsed.kind;
645
+ if (!parsed.known || kind !== "fast") return undefined;
514
646
  const capability = CURSOR_CAPABILITIES[parsed.baseId];
515
647
  if (capability?.wirePrefix !== "cursor-") return undefined;
516
648
  const spec = capability.variants.fast;
@@ -532,6 +664,7 @@ export function cursorUmbrellaRows(): CursorUmbrellaRow[] {
532
664
  if (!spec || spec.quarantined) continue;
533
665
  rows.push({
534
666
  id: baseId,
667
+ displayName: capability.displayName,
535
668
  efforts: spec.levels,
536
669
  window: capability.window,
537
670
  maxModeVerified: capability.maxModeVerified === true,
@@ -5,7 +5,7 @@ import {
5
5
  cursorWireModelIdWithEffort,
6
6
  CURSOR_THINKING_MODEL_IDS,
7
7
  } from "./effort-map";
8
- import { parseCursorVariantId } from "./catalog";
8
+ import { cursorUmbrellaRows, parseCursorVariantId } from "./catalog";
9
9
 
10
10
  export interface CursorModelInfo {
11
11
  id: string;
@@ -30,6 +30,8 @@ export function inferCursorContextWindow(modelId: string): number {
30
30
  if (id.includes("1m")) return CONTEXT_1M;
31
31
  if (id.startsWith("gemini-")) return CONTEXT_1M;
32
32
  if (id === "glm-5.3" || id === "glm-5.2") return CONTEXT_1M;
33
+ // 260902: every Fable is a 1M model; catch live spellings the seed does not carry.
34
+ if (id.includes("fable")) return CONTEXT_1M;
33
35
  if (id.startsWith("gpt-5.6-")) return CONTEXT_1M;
34
36
  if (id.startsWith("gpt-5") || id === "gpt-5-codex") return CONTEXT_272K;
35
37
  if (id.startsWith("grok-4.5") || id.startsWith("grok-4.6")) return 500_000;
@@ -260,96 +262,86 @@ export function filterCursorConfiguredModelsByLiveDiscovery<T extends { id: stri
260
262
  */
261
263
  export const CURSOR_KNOWN_UNCALLABLE_MODEL_IDS: ReadonlySet<string> = new Set([]);
262
264
 
265
+ /**
266
+ * Cursor products that are NOT a dimension of any capability base. Each carries its own
267
+ * label because there is no capability record to read one from. A row belongs here only
268
+ * when Cursor ships it as a distinct product; a variant of a cataloged base does not.
269
+ */
270
+ export const CURSOR_PRODUCT_MODELS: readonly (CursorModelInfo & { displayName: string })[] = [
271
+ { id: "claude-4.5-haiku", displayName: "Claude Haiku 4.5", contextWindow: CONTEXT_200K },
272
+ { id: "composer-1", displayName: "Composer 1", contextWindow: CONTEXT_200K },
273
+ { id: "composer-2.5", displayName: "Composer 2.5", contextWindow: CONTEXT_200K },
274
+ { id: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", contextWindow: CONTEXT_GEMINI },
275
+ { id: "gemini-3-flash", displayName: "Gemini 3 Flash", contextWindow: CONTEXT_GEMINI },
276
+ { id: "gemini-3-pro", displayName: "Gemini 3 Pro", contextWindow: CONTEXT_GEMINI },
277
+ { id: "gemini-3-pro-image-preview", displayName: "Gemini 3 Pro Image", contextWindow: CONTEXT_200K },
278
+ { id: "gemini-3.1-pro", displayName: "Gemini 3.1 Pro", contextWindow: CONTEXT_GEMINI },
279
+ { id: "gemini-3.5-flash", displayName: "Gemini 3.5 Flash", contextWindow: CONTEXT_200K },
280
+ { id: "gpt-5-codex", displayName: "GPT-5 Codex", contextWindow: CONTEXT_272K },
281
+ { id: "gpt-5-mini", displayName: "GPT-5 Mini", contextWindow: CONTEXT_272K },
282
+ { id: "gpt-5.1-codex", displayName: "GPT-5.1 Codex", contextWindow: CONTEXT_272K },
283
+ { id: "kimi-k2.7-code", displayName: "Kimi K2.7 Code", contextWindow: CONTEXT_262K },
284
+ ];
285
+
286
+ /**
287
+ * Real upstream wire ids that LOOK like a dimension of a cataloged base but are served as
288
+ * their own catalog row by Cursor, so they stay rows rather than folding into a base.
289
+ *
290
+ * - `claude-4-sonnet-1m`: a distinct 1M-window row upstream, not `claude-4-sonnet` + ultra.
291
+ * claude-4-sonnet carries no maxMode evidence, so folding it would invent a capability.
292
+ * `REAL_1M_WIRE_IDS` in catalog.ts already stops the parser reading it as the synthetic
293
+ * marker.
294
+ * - `gpt-5-fast`: there is no `gpt-5` capability base for it to be a dimension of.
295
+ * - `composer-2.5-fast`: composer-2.5 has no effort or variant dimensions at all.
296
+ */
297
+ export const CURSOR_REAL_ID_EXCEPTIONS: readonly (CursorModelInfo & { displayName: string })[] = [
298
+ { id: "claude-4-sonnet-1m", displayName: "Claude Sonnet 4 (1M)", contextWindow: CONTEXT_1M },
299
+ { id: "gpt-5-fast", displayName: "GPT-5 Fast", contextWindow: CONTEXT_272K },
300
+ { id: "composer-2.5-fast", displayName: "Composer 2.5 Fast", contextWindow: CONTEXT_200K },
301
+ ];
302
+
303
+ /** Picker labels for the auto-router rows, which have no capability record. */
304
+ const CURSOR_ROUTER_DISPLAY_NAMES: Readonly<Record<string, string>> = {
305
+ auto: "Auto",
306
+ "auto-cost": "Auto (Cost)",
307
+ "auto-balance": "Auto (Balanced)",
308
+ "auto-intelligence": "Auto (Intelligence)",
309
+ };
310
+
311
+ /**
312
+ * The published Cursor row set. DERIVED from CURSOR_CAPABILITIES via cursorUmbrellaRows()
313
+ * (devlog 260902_cursor_unified_identity) so the capability table and the picker can no
314
+ * longer disagree: one row per base, with thinking / fast / synthetic -1m remaining
315
+ * routable aliases that add no rows.
316
+ *
317
+ * Before this, the seed was a hand-maintained list that drifted from the capability table —
318
+ * `cursorUmbrellaRows()` existed but only tests called it, so collapsing a variant changed
319
+ * routing without changing what Codex listed.
320
+ *
321
+ * Windows and effort ladders come from the capability record; the two lists below carry the
322
+ * ids that have no capability record, each with its own label and window.
323
+ */
263
324
  export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorModels([
264
- // Context windows and the model lineup mirror Cursor's public models/pricing docs plus the jawcode
265
- // SOT (../jawcode/packages/ai/src/models.json, `cursor` provider), which mirrors the real
266
- // GetUsableModels catalog. Live discovery is the preferred path when logged in; these ids seed the
267
- // routed Codex catalog and provide a static fallback. Cursor base ids carry no effort suffix here —
268
- // the request builder appends the per-model suffix (see effort-map.ts) and reasoning models
269
- // advertise effort so Codex exposes the tier picker. `supportsReasoningEffort` tracks whether the
270
- // model has *selectable effort tiers* (CURSOR_MODEL_EFFORT_TIERS), NOT merely whether it reasons:
271
- // gemini/grok/kimi-k2.7/gpt-5-mini are reasoning models in the SOT but are sent bare (no tier picker).
272
325
  ...CURSOR_ROUTER_MODEL_IDS.map(id => ({ id, contextWindow: CONTEXT_200K, supportsReasoningEffort: false })),
273
-
274
- // Umbrella seed (devlog 260828_cursor_umbrella_catalog): one row per BASE
275
- // model. Thinking merges into the base (the resolver routes the thinking
276
- // variant); fast / thinking-fast / -1m stay routable as aliases but add no
277
- // rows. Windows follow CURSOR_CAPABILITIES where the base is cataloged.
278
- { id: "claude-sonnet-5", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
279
- { id: "claude-4-sonnet", contextWindow: CONTEXT_200K },
280
- { id: "claude-4-sonnet-1m", contextWindow: CONTEXT_1M },
281
- { id: "claude-4.5-haiku", contextWindow: CONTEXT_200K },
282
- { id: "claude-4.5-sonnet", contextWindow: CONTEXT_200K },
283
- { id: "claude-4.5-opus", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
284
- { id: "claude-4.6-opus", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
285
- { id: "claude-4.6-sonnet", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
286
- { id: "claude-opus-4-7", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
287
- { id: "claude-opus-4-8", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
288
- // claude-opus-5: regular variant is quarantined (not_found on every Run) but
289
- // the umbrella row routes the THINKING variant, which is live — so the base
290
- // row returns to the seed under the umbrella (resolver never sends the
291
- // quarantined regular wire id for the bare slug).
292
- { id: "claude-opus-5", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
293
- { id: "claude-fable-5", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
294
-
295
- { id: "composer-1", contextWindow: CONTEXT_200K },
296
- { id: "composer-2.5", contextWindow: CONTEXT_200K },
297
- { id: "composer-2.5-fast", contextWindow: CONTEXT_200K },
298
-
299
- { id: "gemini-2.5-flash", contextWindow: CONTEXT_GEMINI },
300
- { id: "gemini-3-flash", contextWindow: CONTEXT_GEMINI },
301
- { id: "gemini-3-pro", contextWindow: CONTEXT_GEMINI },
302
- { id: "gemini-3-pro-image-preview", contextWindow: CONTEXT_200K },
303
- { id: "gemini-3.1-pro", contextWindow: CONTEXT_GEMINI },
304
- { id: "gemini-3.5-flash", contextWindow: CONTEXT_200K },
305
- // 260825 live GetUsableModels: both ship only as effort-suffixed ids, so each exposes a tier
306
- // picker. 3.6 is the only Cursor model with a `minimal` rung.
307
- { id: "gemini-3.6-flash", contextWindow: CONTEXT_GEMINI, supportsReasoningEffort: true },
308
- { id: "gemini-3.7-flash", contextWindow: CONTEXT_GEMINI, supportsReasoningEffort: true },
309
-
310
- { id: "gpt-5-codex", contextWindow: CONTEXT_272K },
311
- { id: "gpt-5-fast", contextWindow: CONTEXT_272K },
312
- { id: "gpt-5-mini", contextWindow: CONTEXT_272K },
313
- { id: "gpt-5.1", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
314
- { id: "gpt-5.1-codex", contextWindow: CONTEXT_272K },
315
- { id: "gpt-5.1-codex-max", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
316
- { id: "gpt-5.1-codex-mini", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
317
- { id: "gpt-5.2", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
318
- { id: "gpt-5.2-codex", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
319
- { id: "gpt-5.3-codex", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
320
- { id: "gpt-5.4", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
321
- { id: "gpt-5.4-mini", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
322
- { id: "gpt-5.4-nano", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
323
- { id: "gpt-5.5", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
324
- // gpt-5.5-extra: absent from cursor.com docs but SURVIVES the live GetUsableModels filter
325
- // (account-verified 260709, devlog/model_update/260709_model_refresh/004_live_snapshot.md).
326
- { id: "gpt-5.5-extra", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
327
- { id: "gpt-5.6-sol", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
328
- { id: "gpt-5.6-terra", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
329
- { id: "gpt-5.6-luna", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
330
-
331
- // 260709 refresh: stale grok/composer/kimi/gpt ids dropped per current cursor.com docs; the
332
- // 260709 note: grok-4.5 was deferred; confirmed live 260708 (cursor.com/models, xAI launch).
333
-
334
- // Conflict resolution (260709): keep the refreshed 1M context + kimi-k2.7-code from de12fc8,
335
- // take PR #73's supportsReasoningEffort for glm-5.2 (its effort-map tiers landed with the PR).
336
- { id: "glm-5.2", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
337
- // 260814 preemptive: glm-5.3 seeded ahead of Cursor's lineup update (mirrors glm-5.2).
338
- { id: "glm-5.3", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
339
- { id: "kimi-k2.7-code", contextWindow: CONTEXT_262K },
340
- // kimi-k3: cursor.com/docs/models/kimi-k3; account-verified via GetUsableModels (2026-07-28) —
341
- // ships only as effort-suffixed kimi-k3-{low,high,max}, so the tier picker is exposed.
342
- // kimi-k3 folds the old synthetic kimi-k3-1m row into the umbrella: the base
343
- // is maxModeVerified (user-verified 1M on the Ultra plan, devlog 260826/025),
344
- // so the ultra effort rung arms Max Mode on the wire and the separate picker
345
- // row is gone. cursor/kimi-k3-1m stays routable as an alias.
346
- { id: "kimi-k3", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
347
-
348
- { id: "grok-4.5", contextWindow: 500_000, supportsReasoningEffort: true },
349
- // 260813 preemptive: grok-4.6 seeded ahead of Cursor's lineup update (mirrors grok-4.5).
350
- { id: "grok-4.6", contextWindow: 500_000, supportsReasoningEffort: true },
326
+ ...cursorUmbrellaRows().map(row => ({
327
+ id: row.id,
328
+ contextWindow: row.window,
329
+ supportsReasoningEffort: row.efforts.length > 0,
330
+ })),
331
+ ...CURSOR_PRODUCT_MODELS,
332
+ ...CURSOR_REAL_ID_EXCEPTIONS,
351
333
  ]);
352
334
 
335
+ /** Picker labels for every seeded row, for providers.cursor.modelDisplayNames. */
336
+ export function cursorModelDisplayNames(): Record<string, string> {
337
+ return Object.fromEntries([
338
+ ...CURSOR_ROUTER_MODEL_IDS.map(id => [id, CURSOR_ROUTER_DISPLAY_NAMES[id] ?? id] as const),
339
+ ...cursorUmbrellaRows().map(row => [row.id, row.displayName] as const),
340
+ ...CURSOR_PRODUCT_MODELS.map(model => [model.id, model.displayName] as const),
341
+ ...CURSOR_REAL_ID_EXCEPTIONS.map(model => [model.id, model.displayName] as const),
342
+ ]);
343
+ }
344
+
353
345
  export function cursorModelIds(models: readonly CursorModelInfo[] = CURSOR_STATIC_MODELS): string[] {
354
346
  return normalizeCursorModels(models).map(model => model.id);
355
347
  }
@@ -23,6 +23,11 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
23
23
  // max is always the top tier (canonical order: low < medium < high < xhigh < max), confirmed
24
24
  // against Anthropic's effort ladder docs and Cursor's live model lineup.
25
25
  "claude-fable-5": ["low", "medium", "high", "xhigh", "max"],
26
+ // 260902 preemptive: Fable 5.1 seeded ahead of Cursor's lineup update (mirrors fable-5) under
27
+ // the three spellings Cursor has used for Claude ids.
28
+ "claude-fable-5-1": ["low", "medium", "high", "xhigh", "max"],
29
+ "claude-fable-5.1": ["low", "medium", "high", "xhigh", "max"],
30
+ "claude-5.1-fable": ["low", "medium", "high", "xhigh", "max"],
26
31
  "claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"],
27
32
  // Opus Fast tiers from the 260822 GetUsableModels dump (devlog .../300): the wire
28
33
  // exposes {base-without-fast}-{effort}-fast only; suffix derivation at the bottom of
@@ -49,6 +54,9 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
49
54
  "claude-opus-4-7-thinking-fast": ["low", "medium", "high", "xhigh", "max"],
50
55
  "claude-sonnet-5-thinking": ["low", "medium", "high", "xhigh", "max"],
51
56
  "claude-fable-5-thinking": ["low", "medium", "high", "xhigh", "max"],
57
+ "claude-fable-5-1-thinking": ["low", "medium", "high", "xhigh", "max"],
58
+ "claude-fable-5.1-thinking": ["low", "medium", "high", "xhigh", "max"],
59
+ "claude-5.1-fable-thinking": ["low", "medium", "high", "xhigh", "max"],
52
60
  "claude-4.6-opus-thinking": ["high", "max"],
53
61
  "claude-4.5-opus-thinking": ["high"],
54
62
  "claude-4.6-sonnet-thinking": ["medium"],
@@ -113,6 +121,9 @@ const CURSOR_THINKING_FAMILIES: Readonly<Record<string, { source: string; order:
113
121
  "claude-opus-4-7-thinking-fast": { source: "claude-opus-4-7-fast", order: "thinking-then-effort" },
114
122
  "claude-sonnet-5-thinking": { source: "claude-sonnet-5", order: "thinking-then-effort" },
115
123
  "claude-fable-5-thinking": { source: "claude-fable-5", order: "thinking-then-effort" },
124
+ "claude-fable-5-1-thinking": { source: "claude-fable-5-1", order: "thinking-then-effort" },
125
+ "claude-fable-5.1-thinking": { source: "claude-fable-5.1", order: "thinking-then-effort" },
126
+ "claude-5.1-fable-thinking": { source: "claude-5.1-fable", order: "effort-then-thinking" },
116
127
  "claude-4.6-opus-thinking": { source: "claude-4.6-opus", order: "effort-then-thinking" },
117
128
  "claude-4.5-opus-thinking": { source: "claude-4.5-opus", order: "effort-then-thinking" },
118
129
  "claude-4.6-sonnet-thinking": { source: "claude-4.6-sonnet", order: "effort-then-thinking" },