@yansigit/opencodex 2.33.1 → 2.35.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 (177) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-BjCaHxdz.js +112 -0
  3. package/gui/dist/assets/index-DLkXOXLC.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -3
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +40 -3
  8. package/src/adapters/cursor/call-id.ts +44 -0
  9. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  10. package/src/adapters/cursor/discovery.ts +60 -2
  11. package/src/adapters/cursor/effort-map.ts +79 -1
  12. package/src/adapters/cursor/envelope-echo.ts +9 -9
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +14 -0
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-shell.ts +103 -6
  17. package/src/adapters/cursor/native-exec.ts +20 -0
  18. package/src/adapters/cursor/protobuf-request.ts +74 -39
  19. package/src/adapters/cursor/request-builder.ts +32 -10
  20. package/src/adapters/cursor/tool-definitions.ts +13 -10
  21. package/src/adapters/cursor/tool-result-normalize.ts +35 -3
  22. package/src/adapters/cursor/types.ts +16 -4
  23. package/src/adapters/cursor.ts +97 -70
  24. package/src/adapters/google-antigravity-replay.ts +105 -25
  25. package/src/adapters/google-antigravity-wire.ts +5 -0
  26. package/src/adapters/google-errors.ts +41 -12
  27. package/src/adapters/google-http.ts +12 -11
  28. package/src/adapters/google.ts +156 -65
  29. package/src/adapters/image.ts +1 -1
  30. package/src/adapters/kiro-constants.ts +15 -0
  31. package/src/adapters/kiro-tools.ts +43 -15
  32. package/src/adapters/kiro.ts +54 -9
  33. package/src/adapters/openai-chat.ts +286 -242
  34. package/src/adapters/openai-responses.ts +334 -24
  35. package/src/adapters/run-turn-queue.ts +36 -1
  36. package/src/adapters/tool-catalog-nudge.ts +2 -2
  37. package/src/adapters/xai-tool-schema.ts +436 -0
  38. package/src/bridge.ts +67 -26
  39. package/src/chat/inbound.ts +14 -1
  40. package/src/chat/outbound.ts +15 -7
  41. package/src/claude/agents-inject.ts +8 -1
  42. package/src/claude/outbound.ts +10 -8
  43. package/src/cli/account-api.ts +27 -7
  44. package/src/cli/account-extended.ts +10 -3
  45. package/src/cli/account.ts +29 -5
  46. package/src/cli/alias.ts +66 -0
  47. package/src/cli/claude.ts +26 -1
  48. package/src/cli/dispatch.ts +13 -1
  49. package/src/cli/help.ts +1 -0
  50. package/src/cli/index.ts +5 -0
  51. package/src/cli/init.ts +1 -0
  52. package/src/cli/models-runtime.ts +95 -0
  53. package/src/cli/models.ts +13 -7
  54. package/src/cli/provider-runtime.ts +16 -2
  55. package/src/cli/registry.ts +6 -1
  56. package/src/cli/telemetry-commands.ts +25 -0
  57. package/src/cli/v2.ts +34 -10
  58. package/src/codex/account-pause.ts +2 -1
  59. package/src/codex/account-priority.ts +3 -2
  60. package/src/codex/app-server-processes.ts +80 -6
  61. package/src/codex/auth-api.ts +48 -8
  62. package/src/codex/auth-context.ts +21 -18
  63. package/src/codex/catalog/aggregation.ts +6 -0
  64. package/src/codex/catalog/model-metadata.ts +13 -1
  65. package/src/codex/catalog/native-models.ts +5 -2
  66. package/src/codex/catalog/parsing.ts +16 -0
  67. package/src/codex/catalog/provider-fetch.ts +20 -37
  68. package/src/codex/catalog/sync.ts +127 -2
  69. package/src/codex/catalog.ts +1 -1
  70. package/src/codex/codex-write-lock.ts +3 -1
  71. package/src/codex/convergence-types.ts +1 -1
  72. package/src/codex/convergence.ts +22 -2
  73. package/src/codex/desired-state.ts +2 -2
  74. package/src/codex/desktop-app-restart.ts +18 -5
  75. package/src/codex/inject-coordination.ts +83 -0
  76. package/src/codex/inject.ts +14 -1
  77. package/src/codex/log-guard/inspect.ts +22 -4
  78. package/src/codex/model-entitlements.ts +9 -2
  79. package/src/codex/prompt-layers.ts +371 -25
  80. package/src/codex/prompt-text-probe.ts +238 -0
  81. package/src/codex/quota.ts +123 -18
  82. package/src/codex/routing.ts +9 -0
  83. package/src/codex/subagent-model-fallback.ts +198 -27
  84. package/src/codex/transition-state.ts +107 -8
  85. package/src/combos/types.ts +10 -0
  86. package/src/compatibility/openai-responses.ts +33 -1
  87. package/src/config/autonomous-remediation.ts +21 -0
  88. package/src/config/provider-validation.ts +14 -0
  89. package/src/config/rebase-provenance.ts +68 -0
  90. package/src/config.ts +191 -17
  91. package/src/generated/compatibility-version.json +229 -153
  92. package/src/images/loop.ts +5 -4
  93. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  94. package/src/lab/fabric/producer-child.ts +1 -1
  95. package/src/lib/config-ownership.ts +20 -0
  96. package/src/lib/errors.ts +11 -2
  97. package/src/lib/package-tree-integrity.ts +101 -0
  98. package/src/oauth/aistudio-credentials.ts +65 -0
  99. package/src/oauth/aistudio-native-daemon.ts +54 -0
  100. package/src/oauth/generic-account-failover.ts +231 -0
  101. package/src/oauth/index.ts +57 -5
  102. package/src/oauth/key-providers.ts +10 -1
  103. package/src/oauth/kiro.ts +45 -0
  104. package/src/oauth/login-cli.ts +19 -20
  105. package/src/oauth/types.ts +15 -0
  106. package/src/providers/codex-capacity.ts +5 -2
  107. package/src/providers/command-code-efforts.ts +38 -6
  108. package/src/providers/context-cap.ts +4 -3
  109. package/src/providers/default-aliases.ts +65 -0
  110. package/src/providers/derive.ts +28 -0
  111. package/src/providers/fastwire.ts +7 -1
  112. package/src/providers/model-presets.ts +119 -0
  113. package/src/providers/new-model-policy.ts +146 -0
  114. package/src/providers/provider-id-rewrite.ts +2 -1
  115. package/src/providers/quota.ts +73 -14
  116. package/src/providers/registry.ts +160 -68
  117. package/src/providers/slug-codec.ts +52 -0
  118. package/src/responses/code-mode-helper-compat.ts +50 -0
  119. package/src/responses/custom-tool-compat.ts +34 -10
  120. package/src/responses/parser.ts +4 -0
  121. package/src/responses/schema.ts +5 -1
  122. package/src/responses/thought-signature-replay.ts +17 -0
  123. package/src/router.ts +40 -2
  124. package/src/routing/analytics.ts +1 -0
  125. package/src/routing/quota.ts +10 -0
  126. package/src/server/auth-cors.ts +23 -0
  127. package/src/server/chat-completions.ts +24 -16
  128. package/src/server/chat-native-sse.ts +3 -3
  129. package/src/server/chat-native.ts +30 -11
  130. package/src/server/claude-messages.ts +1 -1
  131. package/src/server/effort-policy.ts +16 -0
  132. package/src/server/index.ts +152 -80
  133. package/src/server/lifecycle.ts +52 -1
  134. package/src/server/management/agent-settings-routes.ts +31 -15
  135. package/src/server/management/codex-prompt-routes.ts +570 -0
  136. package/src/server/management/combo-routes.ts +2 -1
  137. package/src/server/management/config-routes.ts +27 -9
  138. package/src/server/management/context.ts +9 -0
  139. package/src/server/management/logs-usage-routes.ts +1 -1
  140. package/src/server/management/model-routes.ts +266 -0
  141. package/src/server/management/provider-routes.ts +137 -3
  142. package/src/server/management/routing-profile-routes.ts +2 -2
  143. package/src/server/management-api.ts +2 -0
  144. package/src/server/relay-eager.ts +147 -20
  145. package/src/server/relay.ts +251 -19
  146. package/src/server/request-log-conversation.ts +21 -0
  147. package/src/server/request-log.ts +46 -20
  148. package/src/server/responses/collaboration.ts +42 -5
  149. package/src/server/responses/combo-stream-preflight.ts +10 -3
  150. package/src/server/responses/core.ts +571 -137
  151. package/src/server/responses/empty-completion-guard.ts +35 -0
  152. package/src/server/responses/fetch-helpers.ts +14 -6
  153. package/src/server/responses/input-admission.ts +3 -1
  154. package/src/server/responses/passthrough-error.ts +33 -9
  155. package/src/server/responses/responses-field-backfill.ts +105 -13
  156. package/src/server/responses/ws-upstream.ts +35 -5
  157. package/src/server/responses-custom-tool-repair.ts +52 -7
  158. package/src/server/responses-terminal-repair.ts +25 -4
  159. package/src/server/sse-frame-buffer.ts +31 -4
  160. package/src/server/ws-bridge.ts +15 -4
  161. package/src/telemetry/dispatcher.ts +44 -0
  162. package/src/telemetry/fingerprint.ts +24 -0
  163. package/src/telemetry/hook.ts +43 -0
  164. package/src/telemetry/ledger.ts +54 -0
  165. package/src/telemetry/types.ts +23 -0
  166. package/src/types/config.ts +66 -14
  167. package/src/types/provider.ts +77 -0
  168. package/src/types/request.ts +16 -10
  169. package/src/types/tools.ts +20 -25
  170. package/src/types.ts +1 -0
  171. package/src/usage/expected-prices.ts +43 -24
  172. package/src/usage/log.ts +17 -7
  173. package/src/web-search/exa-executor.ts +40 -9
  174. package/src/web-search/loop.ts +5 -4
  175. package/gui/dist/assets/index-CIDo4y4k.js +0 -102
  176. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
  177. package/src/server/aistudio-ws-hub.ts +0 -295
@@ -7,6 +7,7 @@ import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, too
7
7
  import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
8
8
  import type { TranslatorBudget } from "../lib/translator-budget";
9
9
  import { readBoundedResponseBody } from "../lib/bounded-body";
10
+ import { debugDroppedFrame } from "../lib/debug";
10
11
  import { configuredReasoningEfforts } from "../reasoning-effort";
11
12
  import { commandCodeReasoningEfforts, refreshCommandCodeReasoningEfforts } from "../providers/command-code-efforts";
12
13
  import { identifyRoutedModel } from "./identity";
@@ -161,7 +162,8 @@ function wireMessages(messages: OcxMessage[]): Array<Record<string, unknown>> {
161
162
  if (typeof message.content === "string") content.push({ type: "text", text: message.content });
162
163
  else for (const part of message.content) {
163
164
  if (part.type === "text") content.push({ type: "text", text: part.text });
164
- else content.push(wireImagePart(part.imageUrl));
165
+ else if (part.type === "image") content.push(wireImagePart(part.imageUrl));
166
+ else content.push({ type: "text", text: "[video]" });
165
167
  }
166
168
  out.push({ role: "user", content });
167
169
  }
@@ -481,7 +483,7 @@ async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenera
481
483
  let newline = buffer.indexOf("\n");
482
484
  while (newline >= 0) {
483
485
  const line = buffer.slice(0, newline).trim(); buffer = buffer.slice(newline + 1);
484
- if (line) { try { yield JSON.parse(stripEventFrame(line)) as Record<string, unknown>; } catch { /* ignore non-events */ } }
486
+ if (line) yield* decodeEventLine(line);
485
487
  newline = buffer.indexOf("\n");
486
488
  }
487
489
  const residualBytes = encoder.encode(buffer).byteLength;
@@ -492,7 +494,7 @@ async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenera
492
494
  if (done) break;
493
495
  }
494
496
  const final = buffer.trim();
495
- if (final) { try { yield JSON.parse(stripEventFrame(final)) as Record<string, unknown>; } catch { /* ignore */ } }
497
+ if (final) yield* decodeEventLine(final);
496
498
  } finally {
497
499
  budget.releaseRetained(bufferBytes, { kind: "live_transient" });
498
500
  try { await reader.cancel(); } catch { /* already closed */ }
@@ -500,6 +502,41 @@ async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenera
500
502
  }
501
503
  }
502
504
 
505
+ /**
506
+ * Yield one NDJSON line as an event record, or nothing.
507
+ *
508
+ * `JSON.parse("null")` returns `null` instead of throwing, so the `try/catch` around the parse
509
+ * cannot see it and the `event.type` read in parseStream crashed the turn — the #1219 defect, on
510
+ * the one streaming transport the #1240 audit did not cover because it is NDJSON rather than SSE.
511
+ *
512
+ * A frame that does not parse to a record is padding, not an event: drop it and continue exactly
513
+ * as an unparseable line is already dropped, so a stream whose only frames are junk ends in the
514
+ * same single terminal `done` as an empty body. Skipping is what preserves an answer whose deltas
515
+ * have already arrived — the observed #1219 case is `null` padding BETWEEN content deltas, where
516
+ * terminating would discard a complete response (#1240).
517
+ *
518
+ * Note this deliberately makes a junk-only stream a quiet `[done]` where it previously threw. That
519
+ * throw was an unguarded type assumption, not a designed failure signal, and `[done]` is already
520
+ * what an empty body, a blank-line-only body and an unparseable-only body all produce here. The
521
+ * broader question — whether this adapter should report *any* no-valid-event stream as a failure
522
+ * rather than an empty success — is pre-existing, applies to all four of those inputs equally, and
523
+ * is deliberately not decided by this change.
524
+ */
525
+ function* decodeEventLine(line: string): Generator<Record<string, unknown>> {
526
+ let parsed: unknown;
527
+ try {
528
+ parsed = JSON.parse(stripEventFrame(line));
529
+ } catch {
530
+ debugDroppedFrame("command-code", line);
531
+ return;
532
+ }
533
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
534
+ debugDroppedFrame("command-code", line);
535
+ return;
536
+ }
537
+ yield parsed as Record<string, unknown>;
538
+ }
539
+
503
540
  /** The endpoint is newline-delimited JSON; defensively strip an SSE `data:` frame if the gateway ever switches shapes. */
504
541
  function stripEventFrame(line: string): string {
505
542
  return line.startsWith("data:") ? line.slice("data:".length).trim() : line;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Reversible single-line codec for Cursor composite tool-call ids.
3
+ *
4
+ * Cursor's wire delivers tool-call ids that can be two identifiers glued with a
5
+ * literal newline ("call-<uuid>-<n>\nfc_<uuid>_<n>"). OpenCodex forwards ids
6
+ * verbatim, so that newline leaked into Responses-visible `call_id` values,
7
+ * where line-oriented clients (logging, splitting, validation) break. The codec
8
+ * encodes only ids containing CR/LF into a versioned single-line form and
9
+ * decodes both that form and legacy raw multi-line ids back to the exact
10
+ * upstream bytes before anything is serialized toward Cursor.
11
+ */
12
+
13
+ const CALL_ID_PREFIX = "ocxc1_";
14
+
15
+ /** True when the id needs encoding to survive line-oriented consumers. */
16
+ function needsEncoding(id: string): boolean {
17
+ return id.includes("\n") || id.includes("\r");
18
+ }
19
+
20
+ /** Encode a Cursor wire call id into a single-line Responses-safe id. */
21
+ export function encodeCursorCallId(id: string): string {
22
+ if (!needsEncoding(id)) return id;
23
+ return CALL_ID_PREFIX + Buffer.from(id, "utf8").toString("base64url");
24
+ }
25
+
26
+ /**
27
+ * Decode a Responses-visible call id back to the exact Cursor wire id.
28
+ * Non-encoded ids (including legacy raw multi-line ids replayed by older
29
+ * clients) pass through unchanged; a malformed encoded payload also passes
30
+ * through rather than corrupting pairing.
31
+ */
32
+ export function decodeCursorCallId(id: string): string {
33
+ if (!id.startsWith(CALL_ID_PREFIX)) return id;
34
+ const payload = id.slice(CALL_ID_PREFIX.length);
35
+ if (payload.length === 0) return id;
36
+ try {
37
+ const decoded = Buffer.from(payload, "base64url").toString("utf8");
38
+ // Round-trip guard: only trust payloads our encoder could have produced.
39
+ if (Buffer.from(decoded, "utf8").toString("base64url") !== payload) return id;
40
+ return decoded;
41
+ } catch {
42
+ return id;
43
+ }
44
+ }
@@ -236,6 +236,7 @@ export function commitCursorCheckpoint(input: {
236
236
  }
237
237
 
238
238
  export function getCursorCheckpointForPrefix(input: {
239
+ conversationId: string;
239
240
  prefixDigest: string;
240
241
  systemDigest: string;
241
242
  coveredMessageCount: number;
@@ -244,17 +245,21 @@ export function getCursorCheckpointForPrefix(input: {
244
245
  }): CursorCheckpointSnapshot | undefined {
245
246
  prune();
246
247
  const refs = store.prefixIndex.get(input.prefixDigest);
247
- if (!refs || refs.size !== 1) return undefined;
248
- const [ref] = refs;
249
- if (!ref) return undefined;
250
- const snapshot = getCursorCheckpoint(ref);
251
- if (!snapshot) return undefined;
248
+ if (!refs) return undefined;
252
249
  const identityScope = input.identityScope?.trim() || "local";
253
- if (snapshot.systemDigest !== input.systemDigest) return undefined;
254
- if (snapshot.coveredMessageCount !== input.coveredMessageCount) return undefined;
255
- if (snapshot.identityScope !== identityScope) return undefined;
256
- if (snapshot.modelId !== input.modelId) return undefined;
257
- return snapshot;
250
+ let foundRef: string | undefined;
251
+ for (const ref of refs) {
252
+ const snapshot = store.snapshots.get(ref);
253
+ if (!snapshot) continue;
254
+ if (snapshot.conversationId !== input.conversationId) continue;
255
+ if (snapshot.systemDigest !== input.systemDigest) continue;
256
+ if (snapshot.coveredMessageCount !== input.coveredMessageCount) continue;
257
+ if (snapshot.identityScope !== identityScope) continue;
258
+ if (snapshot.modelId !== input.modelId) continue;
259
+ if (foundRef) return undefined;
260
+ foundRef = ref;
261
+ }
262
+ return getCursorCheckpoint(foundRef);
258
263
  }
259
264
 
260
265
  export function getLatestCursorCheckpoint(
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  CANONICAL_EFFORT_SUFFIXES,
3
3
  cursorModelEffortLadder,
4
+ cursorModelHasEffortTiers,
4
5
  cursorWireModelIdWithEffort,
6
+ CURSOR_THINKING_MODEL_IDS,
5
7
  } from "./effort-map";
6
8
 
7
9
  export interface CursorModelInfo {
@@ -152,6 +154,25 @@ export function cursorCodexToWireModelId(modelId: string): string {
152
154
  return cursorWireModelSelection(modelId).modelId;
153
155
  }
154
156
 
157
+ /**
158
+ * Synthetic ultra/big-context picker marker (devlog 260826 070). A `cursor/<base>-1m` row is a
159
+ * picker-only variant: the wire request keeps `<base>` (plus effort suffix) and turns on Cursor
160
+ * Max Mode instead. Only ids listed here are treated as synthetic — a real upstream wire id that
161
+ * happens to end in `-1m` never collides because it will not be in this set.
162
+ */
163
+ export const CURSOR_ULTRA_1M_MODEL_IDS: ReadonlySet<string> = new Set([
164
+ "kimi-k3-1m",
165
+ ]);
166
+
167
+ const CURSOR_ULTRA_1M_SUFFIX = "-1m";
168
+
169
+ /** Resolve a synthetic ultra marker id to its wire base, or undefined for ordinary ids. */
170
+ export function cursorUltraBaseModelId(modelId: string): string | undefined {
171
+ const normalized = modelId.startsWith("cursor/") ? modelId.slice("cursor/".length) : modelId;
172
+ if (!CURSOR_ULTRA_1M_MODEL_IDS.has(normalized)) return undefined;
173
+ return normalized.slice(0, -CURSOR_ULTRA_1M_SUFFIX.length);
174
+ }
175
+
155
176
  /**
156
177
  * Cursor-native wire models keep server-side conversation state reliably.
157
178
  * External models (gpt/claude/gemini/grok families and similar) are more brittle on resumeAction.
@@ -212,10 +233,27 @@ export function filterCursorConfiguredModelsByLiveDiscovery<T extends { id: stri
212
233
  liveIds: readonly string[],
213
234
  ): T[] {
214
235
  return configured.filter(model =>
215
- isCursorRouterModelId(model.id) || isCursorModelAvailableForAccount(model.id, liveIds),
236
+ !CURSOR_KNOWN_UNCALLABLE_MODEL_IDS.has(model.id)
237
+ && (
238
+ isCursorRouterModelId(model.id)
239
+ // Synthetic ultra rows ride their base model's account availability.
240
+ || isCursorModelAvailableForAccount(cursorUltraBaseModelId(model.id) ?? model.id, liveIds)
241
+ ),
216
242
  );
217
243
  }
218
244
 
245
+ /**
246
+ * Models GetUsableModels advertises but whose every Run returns not_found (catalog honesty,
247
+ * devlog 260826_cursor_responses_gap 060). Live probes 2026-08-26: cursor/claude-opus-5 failed
248
+ * 100% ("Cursor Connect error not_found") while its -fast and -thinking siblings — separate
249
+ * wire families — succeed. Quarantined here, in the shared filter, so live, cached, stale, and
250
+ * static serving paths all agree. Custom user provider overrides are not routed through this
251
+ * canonical seed and stay untouched.
252
+ */
253
+ export const CURSOR_KNOWN_UNCALLABLE_MODEL_IDS: ReadonlySet<string> = new Set([
254
+ "claude-opus-5",
255
+ ]);
256
+
219
257
  export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorModels([
220
258
  // Context windows and the model lineup mirror Cursor's public models/pricing docs plus the jawcode
221
259
  // SOT (../jawcode/packages/ai/src/models.json, `cursor` provider), which mirrors the real
@@ -243,7 +281,8 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
243
281
  { id: "claude-opus-4-7-fast", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
244
282
  { id: "claude-opus-4-8-fast", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
245
283
  { id: "claude-opus-4-8", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
246
- { id: "claude-opus-5", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
284
+ // claude-opus-5 (bare) removed from the seed: GetUsableModels lists it but every Run returns
285
+ // not_found (quarantined via CURSOR_KNOWN_UNCALLABLE_MODEL_IDS; -fast/-thinking families stay).
247
286
  { id: "claude-opus-5-fast", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
248
287
  { id: "claude-fable-5", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
249
288
 
@@ -257,6 +296,21 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
257
296
  { id: "gemini-3-pro-image-preview", contextWindow: CONTEXT_200K },
258
297
  { id: "gemini-3.1-pro", contextWindow: CONTEXT_GEMINI },
259
298
  { id: "gemini-3.5-flash", contextWindow: CONTEXT_200K },
299
+ // 260825 live GetUsableModels: both ship only as effort-suffixed ids, so each exposes a tier
300
+ // picker. 3.6 is the only Cursor model with a `minimal` rung.
301
+ { id: "gemini-3.6-flash", contextWindow: CONTEXT_GEMINI, supportsReasoningEffort: true },
302
+ { id: "gemini-3.7-flash", contextWindow: CONTEXT_GEMINI, supportsReasoningEffort: true },
303
+
304
+ // Explicit-thinking variants (260825 live roster). Exposed as first-class ids the same way the
305
+ // Opus Fast families were in 831810c13: `isCursorModelAvailableForAccount` matches a base id
306
+ // against `{base}`, `{base}-{effort}` and the family's wire form, and none of those ever
307
+ // matched a `-thinking` id, so every one of these was invisible in the routed catalog.
308
+ // Suffix ORDER differs per family; `cursorWireModelIdWithEffort` owns that mapping.
309
+ ...CURSOR_THINKING_MODEL_IDS.map(id => ({
310
+ id,
311
+ contextWindow: CONTEXT_200K,
312
+ supportsReasoningEffort: cursorModelHasEffortTiers(id),
313
+ })),
260
314
 
261
315
  { id: "gpt-5-codex", contextWindow: CONTEXT_272K },
262
316
  { id: "gpt-5-fast", contextWindow: CONTEXT_272K },
@@ -291,6 +345,10 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
291
345
  // kimi-k3: cursor.com/docs/models/kimi-k3; account-verified via GetUsableModels (2026-07-28) —
292
346
  // ships only as effort-suffixed kimi-k3-{low,high,max}, so the tier picker is exposed.
293
347
  { id: "kimi-k3", contextWindow: CONTEXT_262K, supportsReasoningEffort: true },
348
+ // kimi-k3-1m: synthetic ultra/Max-Mode picker variant (CURSOR_ULTRA_1M_MODEL_IDS) — wire sends
349
+ // kimi-k3-<effort> with maxMode=true; 1M context user-verified live on the Ultra plan
350
+ // (devlog 260826_cursor_responses_gap/025). inferCursorContextWindow maps "1m" ids to 1M.
351
+ { id: "kimi-k3-1m", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
294
352
 
295
353
  { id: "grok-4.5", contextWindow: 500_000, supportsReasoningEffort: true },
296
354
  { id: "grok-4.5-fast", contextWindow: 500_000, supportsReasoningEffort: true },
@@ -34,12 +34,33 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
34
34
  "claude-opus-5-fast": ["low", "medium", "high"],
35
35
  "claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
36
36
  "glm-5.2": ["high", "max"],
37
+ // 260825 live GetUsableModels. gemini-3.6-flash is the only Cursor model exposing `minimal`;
38
+ // listing it here is also what admits the suffix into CANONICAL_EFFORT_SUFFIXES below.
39
+ "gemini-3.6-flash": ["minimal", "low", "medium", "high"],
40
+ "gemini-3.7-flash": ["low", "medium", "high"],
41
+ // Explicit-thinking variants (260825 live roster). Tiers are the rungs the wire actually
42
+ // lists for each family, which is not always the same set the non-thinking id carries:
43
+ // 4.6-opus thinks only at high/max, 4.5-opus only at high, 4.6-sonnet only at medium.
44
+ "claude-opus-5-thinking": ["low", "medium", "high", "xhigh", "max"],
45
+ "claude-opus-5-thinking-fast": ["low", "medium", "high", "xhigh", "max"],
46
+ "claude-opus-4-8-thinking": ["low", "medium", "high", "xhigh", "max"],
47
+ "claude-opus-4-8-thinking-fast": ["low", "medium", "high", "xhigh", "max"],
48
+ "claude-opus-4-7-thinking": ["low", "medium", "high", "xhigh", "max"],
49
+ "claude-opus-4-7-thinking-fast": ["low", "medium", "high", "xhigh", "max"],
50
+ "claude-sonnet-5-thinking": ["low", "medium", "high", "xhigh", "max"],
51
+ "claude-fable-5-thinking": ["low", "medium", "high", "xhigh", "max"],
52
+ "claude-4.6-opus-thinking": ["high", "max"],
53
+ "claude-4.5-opus-thinking": ["high"],
54
+ "claude-4.6-sonnet-thinking": ["medium"],
37
55
  // 260814 preemptive: glm-5.3 seeded ahead of Cursor's lineup update. Unlike 5.2, Z.AI folds
38
56
  // 5.3 efforts into low/high/max (docs.z.ai/devpack/latest-model), so `low` is a real tier.
39
57
  "glm-5.3": ["low", "high", "max"],
40
58
  // GetUsableModels (2026-07-28) lists kimi-k3 only as effort-suffixed kimi-k3-{low,high,max};
41
59
  // the bare id returns not_found. Tiers mirror the native Kimi provider's K3 ladder.
42
60
  "kimi-k3": ["low", "high", "max"],
61
+ // Synthetic ultra picker variant (devlog 260826 070): same tier ladder as kimi-k3; the -1m
62
+ // marker is stripped before wire-id composition, so these tiers never form a wire suffix.
63
+ "kimi-k3-1m": ["low", "high", "max"],
43
64
  // Cursor renamed the Grok 4.5 slugs to cursor-grok-4.5-{low,medium,high} and
44
65
  // cursor-grok-4.5-{low,medium,high}-fast. The bare Fast id returns not_found.
45
66
  "grok-4.5": ["low", "medium", "high"],
@@ -71,6 +92,46 @@ export const CANONICAL_EFFORT_SUFFIXES: ReadonlySet<string> = new Set([
71
92
 
72
93
  const CANONICAL_CODEX_EFFORT_ORDER = ["low", "medium", "high", "xhigh", "max"] as const;
73
94
 
95
+ /**
96
+ * Cursor's explicit-thinking variants, exposed as first-class Codex model ids the same way the
97
+ * `-fast` families were.
98
+ *
99
+ * `source` is the id whose wire name the variant is built from; `order` is where Cursor puts the
100
+ * thinking marker relative to the effort rung. All three shapes exist in the live roster
101
+ * (GetUsableModels, 260825), and using the wrong one is rejected with ERROR_BAD_MODEL_NAME:
102
+ *
103
+ * thinking-then-effort claude-opus-5-thinking-high, claude-opus-5-thinking-high-fast
104
+ * effort-then-thinking claude-4.6-opus-high-thinking
105
+ * bare claude-4.5-sonnet-thinking (the model has no effort rung)
106
+ */
107
+ const CURSOR_THINKING_FAMILIES: Readonly<Record<string, { source: string; order: "thinking-then-effort" | "effort-then-thinking" | "bare" }>> = {
108
+ "claude-opus-5-thinking": { source: "claude-opus-5", order: "thinking-then-effort" },
109
+ "claude-opus-5-thinking-fast": { source: "claude-opus-5-fast", order: "thinking-then-effort" },
110
+ "claude-opus-4-8-thinking": { source: "claude-opus-4-8", order: "thinking-then-effort" },
111
+ "claude-opus-4-8-thinking-fast": { source: "claude-opus-4-8-fast", order: "thinking-then-effort" },
112
+ "claude-opus-4-7-thinking": { source: "claude-opus-4-7", order: "thinking-then-effort" },
113
+ "claude-opus-4-7-thinking-fast": { source: "claude-opus-4-7-fast", order: "thinking-then-effort" },
114
+ "claude-sonnet-5-thinking": { source: "claude-sonnet-5", order: "thinking-then-effort" },
115
+ "claude-fable-5-thinking": { source: "claude-fable-5", order: "thinking-then-effort" },
116
+ "claude-4.6-opus-thinking": { source: "claude-4.6-opus", order: "effort-then-thinking" },
117
+ "claude-4.5-opus-thinking": { source: "claude-4.5-opus", order: "effort-then-thinking" },
118
+ "claude-4.6-sonnet-thinking": { source: "claude-4.6-sonnet", order: "effort-then-thinking" },
119
+ "claude-4.5-sonnet-thinking": { source: "claude-4.5-sonnet", order: "bare" },
120
+ "claude-4-sonnet-thinking": { source: "claude-4-sonnet", order: "bare" },
121
+ };
122
+
123
+ /** Codex-facing ids for Cursor's explicit-thinking variants. */
124
+ export const CURSOR_THINKING_MODEL_IDS = Object.keys(CURSOR_THINKING_FAMILIES);
125
+
126
+ /**
127
+ * Picker order, which is the canonical ladder plus the declared sentinels that rank below `low`.
128
+ *
129
+ * `cursorModelEffortLadder` filters against this, so a tier absent from it is silently dropped
130
+ * from the Codex picker even though `cursorEffortSuffix` would happily send it. That is what
131
+ * hid `gemini-3.6-flash-minimal`, the one Cursor model with a `minimal` rung.
132
+ */
133
+ const CURSOR_PICKER_EFFORT_ORDER = ["minimal", ...CANONICAL_CODEX_EFFORT_ORDER] as const;
134
+
74
135
  function normalizeRequestedEffort(reasoning: string | undefined): string | undefined {
75
136
  const normalized = reasoning?.toLowerCase();
76
137
  return normalized === "ultra" ? "max" : normalized;
@@ -119,7 +180,7 @@ export function cursorModelEffortLadder(baseModelId: string): string[] | undefin
119
180
  const tiers = CURSOR_MODEL_EFFORT_TIERS[baseModelId];
120
181
  if (!tiers || tiers.length === 0) return undefined;
121
182
  const tierSet = new Set(tiers);
122
- return CANONICAL_CODEX_EFFORT_ORDER.filter(effort => tierSet.has(effort));
183
+ return CURSOR_PICKER_EFFORT_ORDER.filter(effort => tierSet.has(effort));
123
184
  }
124
185
 
125
186
  /** Base models known to carry a reasoning-effort suffix (everything else is sent bare). */
@@ -133,6 +194,23 @@ export function cursorModelHasEffortTiers(baseModelId: string): boolean {
133
194
  * and send the base model plus requested_model parameters instead.
134
195
  */
135
196
  export function cursorWireModelIdWithEffort(baseModelId: string, effortSuffix: string): string {
197
+ const thinking = CURSOR_THINKING_FAMILIES[baseModelId];
198
+ if (thinking) {
199
+ const { source, order } = thinking;
200
+ // Cursor writes the thinking marker on either side of the effort depending on family
201
+ // (measured against GetUsableModels, 260825):
202
+ // thinking-then-effort claude-opus-5-thinking-high, ...-thinking-high-fast
203
+ // effort-then-thinking claude-4.6-opus-high-thinking
204
+ // bare claude-4.5-sonnet-thinking (no effort rung at all)
205
+ // Sending the wrong order returns ERROR_BAD_MODEL_NAME, so this is not cosmetic.
206
+ if (order === "bare") return `${source}-thinking`;
207
+ if (order === "effort-then-thinking") return `${source}-${effortSuffix}-thinking`;
208
+ if (source.endsWith("-fast")) {
209
+ const stem = source.slice(0, -"-fast".length);
210
+ return `${stem}-thinking-${effortSuffix}-fast`;
211
+ }
212
+ return `${source}-thinking-${effortSuffix}`;
213
+ }
136
214
  if (baseModelId.endsWith("-fast")) {
137
215
  return `${baseModelId.slice(0, -"-fast".length)}-${effortSuffix}-fast`;
138
216
  }
@@ -89,17 +89,17 @@ export type RoutingCommentaryDecision =
89
89
  | { kind: "flush" }
90
90
  | { kind: "hallucination" };
91
91
 
92
- const ROUTING_NATIVE_TOOL_NAME = /\b(shell|read|grep|list|bash|filesystem)\b/giu;
92
+ const ROUTING_NATIVE_TOOL_NAME = /\b(shell|read|grep|list|bash)\b/giu;
93
93
  const ROUTING_TOOL_HINT =
94
- /(?:\b(?:shell|read|grep|list|bash|filesystem)\b|exec_command|shell_command|브리지|네이티브\s*(?:셸|쉘))/iu;
95
- const ROUTING_FAILURE_OR_BRIDGE_CLAIM =
96
- /(?:blocked|unavailable|interrupted|bridged|bridge|차단|중단|막혀)/iu;
94
+ /(?:\b(?:shell|read|grep|list|bash)\b|exec_command|shell_command|브리지|네이티브\s*(?:셸|쉘))/iu;
95
+ const ROUTING_FAILURE_CLAIM =
96
+ /(?:blocked|unavailable|interrupted|차단|중단|막혀)/iu;
97
97
  const ROUTING_REDIRECT_CLAIM =
98
- /(?:exec_command|shell_command|\bexec\b|브리지|bridged|bridge|redirected|fallback|switch(?:ed|ing)?|host shell|전환|우회|통과(?:되|하)|다른\s*(?:도구|경로)|경로로)/iu;
98
+ /(?:exec_command|shell_command|\bexec\b|브리지|redirected|fallback|switch(?:ed|ing)?|전환|우회|통과(?:되|하)|다른\s*(?:도구|경로)|경로로)/iu;
99
99
 
100
100
  /**
101
101
  * Quarantines the first line of code-mode / bridge output long enough to reject
102
- * an impossible routing claim. It requires a failure or bridge claim plus either an
102
+ * an impossible routing claim. It requires a failure claim plus either an
103
103
  * explicit redirect to another execution surface or two distinct unadvertised
104
104
  * native-tool names; a legitimate sentence such as "Shell is unavailable on
105
105
  * this OS" therefore passes.
@@ -122,10 +122,10 @@ export class CursorRoutingCommentarySniffer {
122
122
  return { kind: "hallucination" };
123
123
  }
124
124
  const lineBreakCount = (this.buffered.match(/\n/gu) ?? []).length;
125
- const hasRoutingHint = ROUTING_TOOL_HINT.test(this.buffered) || ROUTING_FAILURE_OR_BRIDGE_CLAIM.test(this.buffered);
125
+ const hasRoutingHint = ROUTING_TOOL_HINT.test(this.buffered) || ROUTING_FAILURE_CLAIM.test(this.buffered);
126
126
  const pendingFailureClaim =
127
127
  ROUTING_TOOL_HINT.test(this.buffered)
128
- && ROUTING_FAILURE_OR_BRIDGE_CLAIM.test(this.buffered)
128
+ && ROUTING_FAILURE_CLAIM.test(this.buffered)
129
129
  && lineBreakCount < 2;
130
130
  if (
131
131
  this.byteCount < MAX_ROUTING_COMMENTARY_BYTES
@@ -146,7 +146,7 @@ export class CursorRoutingCommentarySniffer {
146
146
  }
147
147
 
148
148
  private matchesHallucination(): boolean {
149
- if (!ROUTING_FAILURE_OR_BRIDGE_CLAIM.test(this.buffered)) return false;
149
+ if (!ROUTING_FAILURE_CLAIM.test(this.buffered)) return false;
150
150
  const nativeTools = new Set(
151
151
  [...this.buffered.matchAll(ROUTING_NATIVE_TOOL_NAME)].map(match => match[1]?.toLowerCase()),
152
152
  );
@@ -43,7 +43,7 @@ export interface CursorUsableModelsOptions {
43
43
  }
44
44
 
45
45
  export type CursorUsableModelsResult =
46
- | { ok: true; models: string[] }
46
+ | { ok: true; models: string[]; maxModeModels?: string[] }
47
47
  | { ok: false; error: "auth" | "http" | "policy" | "transport" | "timeout" | "decode" | "empty" | "too_large"; detail?: string };
48
48
 
49
49
  /** Test-only seam for management connectivity probes; production callers retain the HTTP/2 path. */
@@ -120,6 +120,7 @@ function decodeCursorUsableModels(bytes: Uint8Array): CursorUsableModelsResult {
120
120
  // make stale configured ids such as `composer-2` look activated.
121
121
  const ids: string[] = [];
122
122
  const seenIds = new Set<string>();
123
+ const maxModeIds: string[] = [];
123
124
  for (const model of response.models ?? []) {
124
125
  const rawId = (model as { modelId?: string }).modelId;
125
126
  if (typeof rawId !== "string") continue;
@@ -127,9 +128,13 @@ function decodeCursorUsableModels(bytes: Uint8Array): CursorUsableModelsResult {
127
128
  if (!isValidModelDiscoveryModelId(id) || seenIds.has(id)) continue;
128
129
  seenIds.add(id);
129
130
  ids.push(id);
131
+ // Preserve Max-Mode capability for ultra/big-context auto-detection (devlog 260826 070).
132
+ if ((model as { maxMode?: boolean }).maxMode === true) maxModeIds.push(id);
130
133
  if (ids.length >= CURSOR_MAX_DISCOVERED_MODELS) break;
131
134
  }
132
- return ids.length > 0 ? { ok: true, models: ids } : { ok: false, error: "empty" };
135
+ return ids.length > 0
136
+ ? { ok: true, models: ids, ...(maxModeIds.length > 0 ? { maxModeModels: maxModeIds } : {}) }
137
+ : { ok: false, error: "empty" };
133
138
  } catch {
134
139
  return { ok: false, error: "decode", detail: "Invalid GetUsableModels protobuf response" };
135
140
  }
@@ -422,6 +422,20 @@ export function finalizeAfterDrain(state: ReturnType<typeof createCursorProtobuf
422
422
  export function clientToolFinalizeGraceMsForRequest(request: CursorRunRequest, baseGraceMs = CLIENT_TOOL_FINALIZE_GRACE_MS): number {
423
423
  if (request.rawMessages?.at(-1)?.role === "toolResult") return baseGraceMs;
424
424
  const text = activePromptText(request);
425
+ // Parallel-tool requests with several advertised tools get the expanded window regardless of
426
+ // prompt shape: external models (grok) assemble sibling calls serially over multiple frames,
427
+ // and the 50ms drain grace ended the turn after 1-2 of them (devlog 260826_cursor_responses_gap,
428
+ // live 10-parallel probe: calls=2 then calls=1).
429
+ if (request.parallelToolCalls === true && (request.tools?.length ?? 0) > 1) {
430
+ const advertised = request.tools?.length ?? 0;
431
+ return Math.max(
432
+ baseGraceMs,
433
+ Math.min(
434
+ GENERIC_TOOL_COUNT_MAX_FINALIZE_GRACE_MS,
435
+ Math.max(GENERIC_TOOL_COUNT_MIN_FINALIZE_GRACE_MS, advertised * GENERIC_TOOL_COUNT_PER_TOOL_GRACE_MS),
436
+ ),
437
+ );
438
+ }
425
439
  if (!cursorRequestHasShellAlias(request.tools) || !isGenericToolUseCountDemoPrompt(text)) return baseGraceMs;
426
440
  const requestedCount = requestedCursorToolUseCount(text);
427
441
  const expandedGraceMs = requestedCount
@@ -1,4 +1,5 @@
1
1
  import type { AdapterEvent } from "../../types";
2
+ import { encodeCursorCallId } from "./call-id";
2
3
  import { cursorExecResult } from "./exec-policy";
3
4
  import type { CursorClientMessage, CursorServerMessage } from "./types";
4
5
  import type { CursorKvStore } from "./kv-store";
@@ -18,7 +19,9 @@ export function mapCursorServerMessage(
18
19
  case "thinking":
19
20
  return [{ type: "thinking_delta", thinking: message.thinking }];
20
21
  case "tool_call_start":
21
- return [{ type: "tool_call_start", id: message.id, name: message.name }];
22
+ // Cursor composite ids can contain a literal newline; Responses call_ids
23
+ // must stay single-line (see call-id.ts).
24
+ return [{ type: "tool_call_start", id: encodeCursorCallId(message.id), name: message.name }];
22
25
  case "tool_call_delta":
23
26
  return [{ type: "tool_call_delta", arguments: message.arguments }];
24
27
  case "tool_call_end":