@bitkyc08/opencodex 2.40.0 → 2.42.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 (97) hide show
  1. package/README.md +4 -0
  2. package/gui/dist/assets/index-BU1tE0sr.js +112 -0
  3. package/gui/dist/assets/index-DL9-iS6J.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/meta.svg +1 -0
  6. package/package.json +4 -3
  7. package/src/adapters/cursor/catalog.ts +71 -29
  8. package/src/adapters/cursor/claude-id.ts +76 -0
  9. package/src/adapters/cursor/discovery.ts +16 -3
  10. package/src/adapters/cursor/effort-map.ts +27 -12
  11. package/src/adapters/cursor/protobuf-request.ts +41 -21
  12. package/src/adapters/google.ts +39 -2
  13. package/src/adapters/identity.ts +8 -2
  14. package/src/adapters/openai-responses.ts +57 -4
  15. package/src/bridge.ts +25 -3
  16. package/src/cli/account-auth.ts +28 -3
  17. package/src/cli/account-extended.ts +7 -1
  18. package/src/cli/capabilities.ts +2 -2
  19. package/src/cli/claude.ts +11 -2
  20. package/src/cli/connect.ts +7 -1
  21. package/src/cli/observe.ts +3 -1
  22. package/src/cli/registry.ts +1 -1
  23. package/src/cli/status.ts +19 -4
  24. package/src/client/connect.ts +5 -1
  25. package/src/client/hub-client.ts +29 -5
  26. package/src/clients/config-export.ts +12 -2
  27. package/src/codex/auth-api.ts +102 -9
  28. package/src/codex/catalog/aggregation.ts +8 -0
  29. package/src/codex/catalog/effort.ts +15 -2
  30. package/src/codex/catalog/metadata.ts +119 -9
  31. package/src/codex/catalog/native-models.ts +71 -0
  32. package/src/codex/catalog/parsing.ts +5 -3
  33. package/src/codex/catalog/provider-fetch.ts +166 -28
  34. package/src/codex/catalog.ts +1 -1
  35. package/src/codex/convergence-types.ts +1 -0
  36. package/src/codex/data/upstream-models.json +169 -0
  37. package/src/codex/desired-state.ts +18 -11
  38. package/src/codex/inject.ts +96 -6
  39. package/src/codex/injected-marker.ts +30 -4
  40. package/src/codex/journal.ts +14 -0
  41. package/src/combos/failover.ts +185 -6
  42. package/src/combos/index.ts +6 -0
  43. package/src/combos/resolve.ts +43 -6
  44. package/src/config.ts +5 -1
  45. package/src/generated/compatibility-version.json +115 -83
  46. package/src/generated/model-metadata.ts +1 -1
  47. package/src/grok/sync.ts +10 -2
  48. package/src/integrations/cursor-effort-table.ts +143 -0
  49. package/src/integrations/state.ts +1 -1
  50. package/src/integrations/writer.ts +2 -2
  51. package/src/lib/app-owned-memory-stores.ts +27 -8
  52. package/src/lib/bounded-body.ts +16 -1
  53. package/src/oauth/account-quota-rank.ts +40 -1
  54. package/src/oauth/chatgpt-device.ts +187 -0
  55. package/src/oauth/chatgpt.ts +31 -4
  56. package/src/oauth/generic-account-failover.ts +2 -2
  57. package/src/oauth/index.ts +24 -3
  58. package/src/oauth/log.ts +3 -0
  59. package/src/oauth/meta-muse.ts +235 -0
  60. package/src/providers/antigravity-models.ts +71 -13
  61. package/src/providers/command-code-efforts.ts +15 -0
  62. package/src/providers/free-directory.ts +4 -1
  63. package/src/providers/muse-subscription-usage.ts +95 -0
  64. package/src/providers/quota.ts +96 -0
  65. package/src/providers/registry.ts +116 -8
  66. package/src/responses/code-mode-helper-compat.ts +4 -1
  67. package/src/responses/state.ts +5 -4
  68. package/src/server/auth-cors.ts +241 -56
  69. package/src/server/chat-completions.ts +11 -2
  70. package/src/server/chat-native.ts +30 -4
  71. package/src/server/claude-messages.ts +17 -3
  72. package/src/server/effort-row.ts +131 -0
  73. package/src/server/index.ts +82 -45
  74. package/src/server/live.ts +18 -4
  75. package/src/server/management/api-key-rotation.ts +2 -1
  76. package/src/server/management/api-key-usage.ts +97 -43
  77. package/src/server/management/context.ts +3 -0
  78. package/src/server/management/cursor-integration-routes.ts +36 -7
  79. package/src/server/management/logs-usage-routes.ts +64 -87
  80. package/src/server/management/oauth-account-routes.ts +10 -3
  81. package/src/server/management/provider-routes.ts +218 -1
  82. package/src/server/management/route-registry.ts +1 -0
  83. package/src/server/management/usage-aggregate-cache.ts +464 -0
  84. package/src/server/management/usage-summary-cache.ts +4 -0
  85. package/src/server/models-capabilities.ts +60 -5
  86. package/src/server/responses/core.ts +95 -7
  87. package/src/server/responses/empty-completion-guard.ts +4 -0
  88. package/src/types/config.ts +10 -1
  89. package/src/types/request.ts +8 -0
  90. package/src/types/tools.ts +12 -9
  91. package/src/usage/expected-prices.ts +43 -7
  92. package/src/usage/ledger-scanner.ts +448 -0
  93. package/src/usage/log.ts +1 -1
  94. package/src/usage/summary.ts +915 -655
  95. package/src/web-search/index.ts +1 -1
  96. package/gui/dist/assets/index-BHe2rl_C.js +0 -112
  97. package/gui/dist/assets/index-CJSb3HPe.css +0 -1
@@ -1,3 +1,9 @@
1
+ import {
2
+ composeCursorClaudeWireId,
3
+ normalizeCursorClaudeId,
4
+ type NormalizedCursorClaudeId,
5
+ } from "./claude-id";
6
+
1
7
  /**
2
8
  * Cursor umbrella catalog — the single source of truth for cursor model
3
9
  * identities (devlog 260828_cursor_umbrella_catalog).
@@ -120,12 +126,8 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
120
126
  thinking: { levels: FULL, order: T },
121
127
  },
122
128
  },
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 has one canonical capability row. Saved aliases and the live roster's
130
+ // exact spelling are normalized and round-tripped at the adapter boundary.
129
131
  "claude-fable-5-1": {
130
132
  displayName: "Claude Fable 5.1",
131
133
  window: CONTEXT_1M,
@@ -135,24 +137,6 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
135
137
  thinking: { levels: FULL, order: T },
136
138
  },
137
139
  },
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
- },
156
140
  "claude-sonnet-5": {
157
141
  displayName: "Claude Sonnet 5",
158
142
  window: CONTEXT_1M,
@@ -221,6 +205,12 @@ export const CURSOR_CAPABILITIES: Record<string, CursorCapability> = {
221
205
  defaultVariant: "regular",
222
206
  variants: { regular: { levels: ["low", "medium", "high"] } },
223
207
  },
208
+ "gemini-3.8-flash": {
209
+ displayName: "Gemini 3.8 Flash",
210
+ window: CONTEXT_GEMINI,
211
+ defaultVariant: "regular",
212
+ variants: { regular: { levels: ["low", "medium", "high"] } },
213
+ },
224
214
  "kimi-k3": {
225
215
  displayName: "Kimi K3",
226
216
  window: CONTEXT_1M,
@@ -382,13 +372,26 @@ const REAL_1M_WIRE_IDS: ReadonlySet<string> = new Set(["claude-4-sonnet-1m"]);
382
372
 
383
373
  export function parseCursorVariantId(rawId: string): ParsedCursorVariantId {
384
374
  const id = rawId.trim();
375
+ if (REAL_1M_WIRE_IDS.has(id)) {
376
+ return { baseId: id, kind: "regular", ultra: false, known: false };
377
+ }
378
+ const claude = normalizeCursorClaudeId(id);
379
+ if (claude && CURSOR_CAPABILITIES[claude.canonicalBaseId]) {
380
+ const explicitVariant = claude.thinking || claude.fast || claude.level !== undefined;
381
+ return {
382
+ baseId: claude.canonicalBaseId,
383
+ kind: explicitVariant
384
+ ? claude.thinking ? (claude.fast ? "thinkingFast" : "thinking") : claude.fast ? "fast" : "regular"
385
+ : defaultKindFor(claude.canonicalBaseId),
386
+ ...(claude.level ? { level: claude.level } : {}),
387
+ ultra: false,
388
+ known: true,
389
+ };
390
+ }
385
391
  // 1. Exact base identity.
386
392
  if (CURSOR_CAPABILITIES[id]) {
387
393
  return { baseId: id, kind: defaultKindFor(id), ultra: false, known: true };
388
394
  }
389
- if (REAL_1M_WIRE_IDS.has(id)) {
390
- return { baseId: id, kind: "regular", ultra: false, known: false };
391
- }
392
395
  // 2. cursor- wire prefix (regular grok wire forms).
393
396
  if (id.startsWith("cursor-")) {
394
397
  const inner = parseCursorVariantId(id.slice("cursor-".length));
@@ -537,17 +540,32 @@ export interface CursorResolvedSelection {
537
540
  readonly known: boolean;
538
541
  }
539
542
 
543
+ type CursorLiveClaudeWireIdentity = Pick<NormalizedCursorClaudeId, "sourceBaseId" | "spelling">;
544
+
540
545
  /**
541
546
  * Compose a variant's flattened wire id, reproducing the legacy effort-map
542
547
  * order rules exactly (thinking-then-effort / effort-then-thinking / bare;
543
548
  * fast marker terminal; wrong order is ERROR_BAD_MODEL_NAME on the wire).
544
549
  */
545
- function composeWireId(baseId: string, kind: CursorVariantKind, effort: string | undefined): string {
550
+ function composeWireId(
551
+ baseId: string,
552
+ kind: CursorVariantKind,
553
+ effort: string | undefined,
554
+ claudeIdentity?: CursorLiveClaudeWireIdentity,
555
+ ): string {
546
556
  const capability = CURSOR_CAPABILITIES[baseId];
547
557
  const spec = capability?.variants[kind];
548
558
  if (!capability || !spec) return baseId;
549
559
  const thinking = kind === "thinking" || kind === "thinkingFast";
550
560
  const fast = kind === "fast" || kind === "thinkingFast";
561
+ if (claudeIdentity) {
562
+ return composeCursorClaudeWireId(claudeIdentity, {
563
+ thinking,
564
+ fast,
565
+ effort,
566
+ bareThinking: spec.order === "bare",
567
+ });
568
+ }
551
569
  if (thinking) {
552
570
  const order = spec.order ?? "thinking-then-effort";
553
571
  if (order === "bare" || effort === undefined) return `${baseId}-thinking`;
@@ -587,7 +605,12 @@ export function resolveCursorSelection(
587
605
  }
588
606
  const requested = parsed.level ?? reasoning;
589
607
  const effort = cursorVariantEffort(spec, requested);
590
- const canonicalId = composeWireId(parsed.baseId, kind, effort);
608
+ const requestedClaude = normalizeCursorClaudeId(pickedId);
609
+ const claudeIdentity = liveCursorClaudeWireIdentities.get(parsed.baseId)
610
+ ?? (requestedClaude
611
+ ? { sourceBaseId: requestedClaude.sourceBaseId, spelling: requestedClaude.spelling }
612
+ : undefined);
613
+ const canonicalId = composeWireId(parsed.baseId, kind, effort, claudeIdentity);
591
614
  const wireId = capability.wirePrefix && kind === "regular"
592
615
  ? `${capability.wirePrefix}${canonicalId}`
593
616
  : canonicalId;
@@ -604,6 +627,25 @@ export function resolveCursorSelection(
604
627
  * arrives — never from window size (devlog 260828 blocker-4 fold).
605
628
  */
606
629
  let liveCursorMaxModeBases: ReadonlySet<string> = new Set();
630
+ let liveCursorClaudeWireIdentities: ReadonlyMap<string, CursorLiveClaudeWireIdentity> = new Map();
631
+
632
+ export function recordLiveCursorClaudeModels(liveIds: readonly string[]): void {
633
+ const next = new Map<string, CursorLiveClaudeWireIdentity>();
634
+ for (const rawId of liveIds) {
635
+ const n = normalizeCursorClaudeId(rawId.startsWith("cursor-") ? rawId.slice(7) : rawId);
636
+ if (!n || !CURSOR_CAPABILITIES[n.canonicalBaseId]) continue;
637
+ if (!next.has(n.canonicalBaseId)) next.set(n.canonicalBaseId, { sourceBaseId: n.sourceBaseId, spelling: n.spelling });
638
+ }
639
+ liveCursorClaudeWireIdentities = next;
640
+ }
641
+
642
+ export function liveCursorClaudeWireIdentitiesForTests(): ReadonlyMap<string, CursorLiveClaudeWireIdentity> {
643
+ return liveCursorClaudeWireIdentities;
644
+ }
645
+
646
+ export function resetLiveCursorClaudeWireIdentitiesForTests(): void {
647
+ liveCursorClaudeWireIdentities = new Map();
648
+ }
607
649
 
608
650
  export function recordLiveCursorMaxModeModels(liveIds: readonly string[]): void {
609
651
  const bases = new Set<string>();
@@ -0,0 +1,76 @@
1
+ export type CursorClaudeSpelling = "anthropic" | "version-first";
2
+
3
+ export interface NormalizedCursorClaudeId {
4
+ /** The sole key used by CURSOR_CAPABILITIES and pricing metadata. */
5
+ canonicalBaseId: string;
6
+ /** Exact input stem, preserving `5-1` versus `5.1` for wire round-trips. */
7
+ sourceBaseId: string;
8
+ spelling: CursorClaudeSpelling;
9
+ thinking: boolean;
10
+ fast: boolean;
11
+ level?: string;
12
+ }
13
+
14
+ const CLAUDE_LEVELS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max", "extra-high"]);
15
+
16
+ /** Existing picker bases whose canonical key stays version-first (saved configs). */
17
+ const VERSION_FIRST_CANONICAL_BASES = new Set([
18
+ "claude-4.5-haiku", "claude-4.5-opus", "claude-4.6-opus", "claude-4.5-sonnet", "claude-4.6-sonnet", "claude-4-sonnet",
19
+ ]);
20
+
21
+ function parseClaudeBase(raw: string): { canonicalBaseId: string; sourceBaseId: string; spelling: CursorClaudeSpelling } | undefined {
22
+ const anthropic = /^claude-(fable|haiku|opus|sonnet)-(\d+(?:[.-]\d+)*)$/.exec(raw);
23
+ if (anthropic) {
24
+ const family = anthropic[1]!;
25
+ const version = anthropic[2]!.replaceAll(".", "-");
26
+ const versionFirst = `claude-${version.replaceAll("-", ".")}-${family}`;
27
+ return {
28
+ canonicalBaseId: VERSION_FIRST_CANONICAL_BASES.has(versionFirst) ? versionFirst : `claude-${family}-${version}`,
29
+ sourceBaseId: raw,
30
+ spelling: "anthropic",
31
+ };
32
+ }
33
+ const versionFirst = /^claude-(\d+(?:\.\d+)*)-(fable|haiku|opus|sonnet)$/.exec(raw);
34
+ if (!versionFirst) return undefined;
35
+ const sourceBaseId = `claude-${versionFirst[1]!}-${versionFirst[2]!}`;
36
+ return {
37
+ canonicalBaseId: VERSION_FIRST_CANONICAL_BASES.has(sourceBaseId) ? sourceBaseId : `claude-${versionFirst[2]!}-${versionFirst[1]!.replaceAll(".", "-")}`,
38
+ sourceBaseId,
39
+ spelling: "version-first",
40
+ };
41
+ }
42
+
43
+ export function normalizeCursorClaudeId(raw: string): NormalizedCursorClaudeId | undefined {
44
+ const id = raw.trim().toLowerCase();
45
+ const patterns: ReadonlyArray<readonly [RegExp, (m: RegExpExecArray) => { base: string; thinking: boolean; fast: boolean; level?: string }]> = [
46
+ [/^(.*)-thinking-([a-z-]+)-fast$/, m => ({ base: m[1]!, thinking: true, fast: true, level: m[2]! })],
47
+ [/^(.*)-([a-z-]+)-thinking-fast$/, m => ({ base: m[1]!, thinking: true, fast: true, level: m[2]! })],
48
+ [/^(.*)-thinking-([a-z-]+)$/, m => ({ base: m[1]!, thinking: true, fast: false, level: m[2]! })],
49
+ [/^(.*)-([a-z-]+)-thinking$/, m => ({ base: m[1]!, thinking: true, fast: false, level: m[2]! })],
50
+ [/^(.*)-([a-z-]+)-fast$/, m => ({ base: m[1]!, thinking: false, fast: true, level: m[2]! })],
51
+ [/^(.*)-thinking-fast$/, m => ({ base: m[1]!, thinking: true, fast: true })],
52
+ [/^(.*)-thinking$/, m => ({ base: m[1]!, thinking: true, fast: false })],
53
+ [/^(.*)-fast$/, m => ({ base: m[1]!, thinking: false, fast: true })],
54
+ ];
55
+ for (const [pattern, dims] of patterns) {
56
+ const match = pattern.exec(id);
57
+ if (!match) continue;
58
+ const parsed = dims(match);
59
+ if (parsed.level && !CLAUDE_LEVELS.has(parsed.level)) continue;
60
+ const base = parseClaudeBase(parsed.base);
61
+ if (base) return { ...base, ...parsed, sourceBaseId: base.sourceBaseId };
62
+ }
63
+ const base = parseClaudeBase(id);
64
+ return base ? { ...base, thinking: false, fast: false } : undefined;
65
+ }
66
+
67
+ export function composeCursorClaudeWireId(
68
+ identity: Pick<NormalizedCursorClaudeId, "sourceBaseId" | "spelling">,
69
+ options: { thinking: boolean; fast: boolean; effort?: string; bareThinking?: boolean },
70
+ ): string {
71
+ const { sourceBaseId: base, spelling } = identity;
72
+ const fast = options.fast ? "-fast" : "";
73
+ if (!options.thinking) return options.effort ? `${base}-${options.effort}${fast}` : `${base}${fast}`;
74
+ if (options.bareThinking || !options.effort) return `${base}-thinking${fast}`;
75
+ return spelling === "version-first" ? `${base}-${options.effort}-thinking${fast}` : `${base}-thinking-${options.effort}${fast}`;
76
+ }
@@ -332,14 +332,27 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
332
332
  ...CURSOR_REAL_ID_EXCEPTIONS,
333
333
  ]);
334
334
 
335
- /** Picker labels for every seeded row, for providers.cursor.modelDisplayNames. */
335
+ /**
336
+ * Picker labels for providers.cursor.modelDisplayNames.
337
+ *
338
+ * Only labels that carry Cursor's own product name ("Cursor Grok 4.6") are published. Every
339
+ * other row keeps the routed `cursor/<id>` slug that the rest of the picker uses, so a Cursor
340
+ * row reads like its siblings from other providers instead of an unprefixed marketing name.
341
+ * #3222 labeled every row and that dropped the `cursor/` prefix from the picker.
342
+ */
336
343
  export function cursorModelDisplayNames(): Record<string, string> {
337
- return Object.fromEntries([
344
+ const labels: (readonly [string, string])[] = [
338
345
  ...CURSOR_ROUTER_MODEL_IDS.map(id => [id, CURSOR_ROUTER_DISPLAY_NAMES[id] ?? id] as const),
339
346
  ...cursorUmbrellaRows().map(row => [row.id, row.displayName] as const),
340
347
  ...CURSOR_PRODUCT_MODELS.map(model => [model.id, model.displayName] as const),
341
348
  ...CURSOR_REAL_ID_EXCEPTIONS.map(model => [model.id, model.displayName] as const),
342
- ]);
349
+ ];
350
+ return Object.fromEntries(labels.filter(([, label]) => isCursorBrandedLabel(label)));
351
+ }
352
+
353
+ /** A label Cursor itself brands with its name, e.g. "Cursor Grok 4.6". */
354
+ export function isCursorBrandedLabel(label: string): boolean {
355
+ return /^cursor\b/i.test(label.trim());
343
356
  }
344
357
 
345
358
  export function cursorModelIds(models: readonly CursorModelInfo[] = CURSOR_STATIC_MODELS): string[] {
@@ -1,3 +1,5 @@
1
+ import { composeCursorClaudeWireId, normalizeCursorClaudeId } from "./claude-id";
2
+
1
3
  /**
2
4
  * Per-model Cursor reasoning-effort mapping.
3
5
  *
@@ -23,11 +25,8 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
23
25
  // max is always the top tier (canonical order: low < medium < high < xhigh < max), confirmed
24
26
  // against Anthropic's effort ladder docs and Cursor's live model lineup.
25
27
  "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
+ // Fable 5.1 aliases normalize onto this sole capability ladder.
28
29
  "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"],
31
30
  "claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"],
32
31
  // Opus Fast tiers from the 260822 GetUsableModels dump (devlog .../300): the wire
33
32
  // exposes {base-without-fast}-{effort}-fast only; suffix derivation at the bottom of
@@ -43,6 +42,10 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
43
42
  // listing it here is also what admits the suffix into CANONICAL_EFFORT_SUFFIXES below.
44
43
  "gemini-3.6-flash": ["minimal", "low", "medium", "high"],
45
44
  "gemini-3.7-flash": ["low", "medium", "high"],
45
+ // 260903 preemptive: gemini-3.8-flash seeded ahead of Cursor's lineup update, the same way
46
+ // glm-5.3 was. Google documents low/medium/high with no `minimal` for this generation,
47
+ // unlike 3.6. The seed is inert until Cursor's live roster lists the id.
48
+ "gemini-3.8-flash": ["low", "medium", "high"],
46
49
  // Explicit-thinking variants (260825 live roster). Tiers are the rungs the wire actually
47
50
  // lists for each family, which is not always the same set the non-thinking id carries:
48
51
  // 4.6-opus thinks only at high/max, 4.5-opus only at high, 4.6-sonnet only at medium.
@@ -55,8 +58,6 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
55
58
  "claude-sonnet-5-thinking": ["low", "medium", "high", "xhigh", "max"],
56
59
  "claude-fable-5-thinking": ["low", "medium", "high", "xhigh", "max"],
57
60
  "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"],
60
61
  "claude-4.6-opus-thinking": ["high", "max"],
61
62
  "claude-4.5-opus-thinking": ["high"],
62
63
  "claude-4.6-sonnet-thinking": ["medium"],
@@ -122,8 +123,6 @@ const CURSOR_THINKING_FAMILIES: Readonly<Record<string, { source: string; order:
122
123
  "claude-sonnet-5-thinking": { source: "claude-sonnet-5", order: "thinking-then-effort" },
123
124
  "claude-fable-5-thinking": { source: "claude-fable-5", order: "thinking-then-effort" },
124
125
  "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" },
127
126
  "claude-4.6-opus-thinking": { source: "claude-4.6-opus", order: "effort-then-thinking" },
128
127
  "claude-4.5-opus-thinking": { source: "claude-4.5-opus", order: "effort-then-thinking" },
129
128
  "claude-4.6-sonnet-thinking": { source: "claude-4.6-sonnet", order: "effort-then-thinking" },
@@ -148,6 +147,12 @@ function normalizeRequestedEffort(reasoning: string | undefined): string | undef
148
147
  return normalized === "ultra" ? "max" : normalized;
149
148
  }
150
149
 
150
+ function cursorEffortLookupId(modelId: string): string {
151
+ const claude = normalizeCursorClaudeId(modelId);
152
+ if (!claude) return modelId;
153
+ return `${claude.canonicalBaseId}${claude.thinking ? "-thinking" : ""}${claude.fast ? "-fast" : ""}`;
154
+ }
155
+
151
156
  /** Collapse a Codex reasoning-effort label to a low/medium/high rank for clamping onto a model's tiers. */
152
157
  function codexEffortRank(reasoning: string | undefined): "low" | "medium" | "high" {
153
158
  switch (normalizeRequestedEffort(reasoning) ?? "") {
@@ -172,7 +177,7 @@ function codexEffortRank(reasoning: string | undefined): "low" | "medium" | "hig
172
177
  * the model takes no suffix (bare). Literal model tiers pass through; unknown efforts clamp by rank.
173
178
  */
174
179
  export function cursorEffortSuffix(baseModelId: string, reasoning: string | undefined): string | undefined {
175
- const tiers = CURSOR_MODEL_EFFORT_TIERS[baseModelId];
180
+ const tiers = CURSOR_MODEL_EFFORT_TIERS[cursorEffortLookupId(baseModelId)];
176
181
  if (!tiers || tiers.length === 0) return undefined;
177
182
  const requested = normalizeRequestedEffort(reasoning);
178
183
  if (requested && tiers.includes(requested)) return requested;
@@ -188,7 +193,7 @@ export function cursorEffortSuffix(baseModelId: string, reasoning: string | unde
188
193
 
189
194
  /** The Codex-facing picker ladder for a Cursor model, sorted in canonical Codex effort order. */
190
195
  export function cursorModelEffortLadder(baseModelId: string): string[] | undefined {
191
- const tiers = CURSOR_MODEL_EFFORT_TIERS[baseModelId];
196
+ const tiers = CURSOR_MODEL_EFFORT_TIERS[cursorEffortLookupId(baseModelId)];
192
197
  if (!tiers || tiers.length === 0) return undefined;
193
198
  const tierSet = new Set(tiers);
194
199
  return CURSOR_PICKER_EFFORT_ORDER.filter(effort => tierSet.has(effort));
@@ -196,7 +201,7 @@ export function cursorModelEffortLadder(baseModelId: string): string[] | undefin
196
201
 
197
202
  /** Base models known to carry a reasoning-effort suffix (everything else is sent bare). */
198
203
  export function cursorModelHasEffortTiers(baseModelId: string): boolean {
199
- return (CURSOR_MODEL_EFFORT_TIERS[baseModelId]?.length ?? 0) > 0;
204
+ return (CURSOR_MODEL_EFFORT_TIERS[cursorEffortLookupId(baseModelId)]?.length ?? 0) > 0;
200
205
  }
201
206
 
202
207
  /**
@@ -205,7 +210,17 @@ export function cursorModelHasEffortTiers(baseModelId: string): boolean {
205
210
  * and send the base model plus requested_model parameters instead.
206
211
  */
207
212
  export function cursorWireModelIdWithEffort(baseModelId: string, effortSuffix: string): string {
208
- const thinking = CURSOR_THINKING_FAMILIES[baseModelId];
213
+ const lookupId = cursorEffortLookupId(baseModelId);
214
+ const thinking = CURSOR_THINKING_FAMILIES[lookupId];
215
+ const claude = normalizeCursorClaudeId(baseModelId);
216
+ if (claude) {
217
+ return composeCursorClaudeWireId(claude, {
218
+ thinking: claude.thinking,
219
+ fast: claude.fast,
220
+ effort: effortSuffix,
221
+ bareThinking: thinking?.order === "bare",
222
+ });
223
+ }
209
224
  if (thinking) {
210
225
  const { source, order } = thinking;
211
226
  // Cursor writes the thinking marker on either side of the effort depending on family
@@ -298,38 +298,40 @@ function rootPromptMessages(
298
298
  // Repetition breaker (devlog 260826 gap-9): external full-replay flattens history to text,
299
299
  // so N identical assistant/tool-result rounds replay as N identical lines and PRIME the model
300
300
  // to emit the same line again (self-reinforcing loop: S2a 180x, identical-probe repetition).
301
- // Collapse consecutive duplicates into one entry + a count marker, and count collapses so a
302
- // strategy-change note can be appended when the pattern is severe.
303
- let lastReplayText: string | undefined;
304
- let lastReplayEntry: RootBlobCandidate | undefined;
305
- let collapsedRepeats = 0;
301
+ // Collapse consecutive same-role duplicates within one user turn into one entry + a count marker.
302
+ // Track assistant narration separately from tool results so a real narration→tool→result cycle
303
+ // cannot reset the breaker before the next identical narration arrives.
304
+ const replayRuns = new Map<RootBlobCandidate["role"], {
305
+ text: string;
306
+ entry: RootBlobCandidate;
307
+ length: number;
308
+ }>();
309
+ const toolCallCounts = new Map<string, number>();
306
310
  let maxRunLength = 1;
307
- let currentRun = 1;
311
+ let maxToolCallCount = 1;
308
312
  const pushDeduped = (
309
313
  payload: { role: string; content: [{ type: "text"; text: string }] },
310
314
  role: RootBlobCandidate["role"],
311
315
  opts: { messageIndex: number; text?: string },
312
316
  normalized: string,
313
317
  ): void => {
314
- if (externalModel && lastReplayText !== undefined && normalized === lastReplayText && lastReplayEntry) {
315
- collapsedRepeats++;
316
- currentRun++;
317
- if (currentRun > maxRunLength) maxRunLength = currentRun;
318
- const marked = `${normalized}\n[note: this exact output was produced ${currentRun} times in a row]`;
318
+ const previous = replayRuns.get(role);
319
+ if (externalModel && previous?.text === normalized) {
320
+ const runLength = previous.length + 1;
321
+ if (runLength > maxRunLength) maxRunLength = runLength;
322
+ const marked = `${normalized}\n[note: this exact output was produced ${runLength} times in a row]`;
319
323
  const replacement = rootBlobCandidate(
320
324
  { role: payload.role, content: [{ type: "text", text: marked }] },
321
325
  role,
322
- opts,
326
+ { ...opts, messageIndex: previous.entry.messageIndex ?? opts.messageIndex },
323
327
  );
324
- entries[entries.indexOf(lastReplayEntry)] = replacement;
325
- lastReplayEntry = replacement;
328
+ entries[entries.indexOf(previous.entry)] = replacement;
329
+ replayRuns.set(role, { text: normalized, entry: replacement, length: runLength });
326
330
  return;
327
331
  }
328
- currentRun = 1;
329
332
  const entry = rootBlobCandidate(payload, role, opts);
330
333
  entries.push(entry);
331
- lastReplayText = normalized;
332
- lastReplayEntry = entry;
334
+ replayRuns.set(role, { text: normalized, entry, length: 1 });
333
335
  };
334
336
 
335
337
  for (let i = 0; i < messages.length; i++) {
@@ -337,14 +339,13 @@ function rootPromptMessages(
337
339
  const message = messages[i];
338
340
  if (!message) continue;
339
341
  if (message.role === "user" || message.role === "developer") {
342
+ replayRuns.clear();
343
+ toolCallCounts.clear();
340
344
  const text = historyContentText(message).trim();
341
345
  // Cursor root replay expects OpenAI-style content parts for historical user messages.
342
346
  // A bare string survives blob hydration but external workers reject the completed replay
343
347
  // before tokenization (`usedTokens: 0`, then invalid_argument).
344
348
  if (text.length > 0) {
345
- lastReplayText = undefined;
346
- lastReplayEntry = undefined;
347
- currentRun = 1;
348
349
  entries.push(rootBlobCandidate({
349
350
  role: "user",
350
351
  content: [{ type: "text", text }],
@@ -362,6 +363,20 @@ function rootPromptMessages(
362
363
  text,
363
364
  );
364
365
  }
366
+ if (externalModel && Array.isArray(message.content)) {
367
+ const callsInMessage = new Set<string>();
368
+ for (const part of message.content) {
369
+ if (part.type !== "toolCall") continue;
370
+ const args = serializeToolCallArguments(part.arguments);
371
+ if (args === undefined) continue;
372
+ callsInMessage.add(JSON.stringify([namespacedToolName(part.namespace, part.name), args]));
373
+ }
374
+ for (const call of callsInMessage) {
375
+ const count = (toolCallCounts.get(call) ?? 0) + 1;
376
+ toolCallCounts.set(call, count);
377
+ if (count > maxToolCallCount) maxToolCallCount = count;
378
+ }
379
+ }
365
380
  // Assistant tool CALLS are NOT replayed as a separate visible "[Tool Call]" entry: a model
366
381
  // few-shot-mimics that marker and emits later tool calls as inert text (363-B guard in
367
382
  // tests/cursor-tool-continuation.test.ts). The invocation is instead named INSIDE the paired
@@ -382,7 +397,12 @@ function rootPromptMessages(
382
397
  }
383
398
  }
384
399
  // Severe repetition: tell the model ONCE, imperatively, to change strategy.
385
- if (externalModel && maxRunLength >= 3) {
400
+ if (externalModel && maxToolCallCount >= 3) {
401
+ entries.push(rootBlobCandidate({
402
+ role: "user",
403
+ content: [{ type: "text", text: `[context note] The transcript above contains the same tool call repeated ${maxToolCallCount} times in this user turn. Repeating it again is a failure. Take a DIFFERENT action now, or state plainly what is blocking progress.` }],
404
+ }, "user", {}));
405
+ } else if (externalModel && maxRunLength >= 3) {
386
406
  entries.push(rootBlobCandidate({
387
407
  role: "user",
388
408
  content: [{ type: "text", text: `[context note] The transcript above contains the same output repeated ${maxRunLength} times in a row. Repeating it again is a failure. Take a DIFFERENT action now, or state plainly what is blocking progress.` }],
@@ -30,7 +30,7 @@ import {
30
30
  clearAntigravityReplay,
31
31
  observeAntigravityReplay,
32
32
  } from "./google-antigravity-replay";
33
- import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
33
+ import { canonicalAntigravityUsageModel, resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
34
34
  import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
35
35
  import { forgetThoughtSignatureForReplay, lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
36
36
  import {
@@ -57,6 +57,43 @@ const GOOGLE_BREVITY_INSTRUCTION = [
57
57
  const ANTIGRAVITY_REJECTED_CLAUDE_SDK_PARAGRAPH =
58
58
  "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
59
59
 
60
+ /**
61
+ * CCA Flash generations that reject the Claude-Agent identity paragraph.
62
+ *
63
+ * Membership is probe-established per generation, never assumed: 3.7 and 3.8 both answer
64
+ * 429 RESOURCE_EXHAUSTED when this paragraph survives into `systemInstruction`, and 200 with
65
+ * it stripped — same account, seconds apart. A policy rejection wearing a quota error's
66
+ * clothing sends users hunting a quota problem that does not exist, so a new generation is
67
+ * added here only after the probe, and never dropped on the assumption that Google fixed it.
68
+ */
69
+ const ANTIGRAVITY_CLAUDE_SDK_PARAGRAPH_REJECTORS = new Set([
70
+ "gemini-3.7-flash",
71
+ "gemini-3.8-flash",
72
+ ]);
73
+
74
+ /**
75
+ * Whether CCA rejects the Claude-Agent identity paragraph for this request.
76
+ *
77
+ * Judged on the ROUTED WIRE id, not the selector, because three different selectors reach the
78
+ * same rejecting generation:
79
+ *
80
+ * - the collapsed base (`gemini-3.8-flash`);
81
+ * - a raw suffix id (`gemini-3.8-flash-high`), which the picker publishes whenever discovery
82
+ * returns a PARTIAL ladder;
83
+ * - a RETIRED id (`gemini-3.6-flash`), which rule 0 redirects onto `gemini-3.7-flash-tiered`.
84
+ *
85
+ * That last one is why a selector-keyed test is not enough: retired ids deliberately keep their
86
+ * own identity for usage accounting, so they never canonicalize into the generation they
87
+ * actually call. A saved 3.6 selection was probed at 429 with the paragraph intact for exactly
88
+ * this reason. Matching on the wire id also means a future generation is covered by naming its
89
+ * wire spelling once, rather than every selector that can reach it.
90
+ */
91
+ function rejectsClaudeSdkParagraph(modelId: string, wireModelId: string): boolean {
92
+ const canonicalWire = canonicalAntigravityUsageModel(wireModelId.replace(/-tiered$/, ""));
93
+ return ANTIGRAVITY_CLAUDE_SDK_PARAGRAPH_REJECTORS.has(canonicalWire)
94
+ || ANTIGRAVITY_CLAUDE_SDK_PARAGRAPH_REJECTORS.has(canonicalAntigravityUsageModel(modelId));
95
+ }
96
+
60
97
  function stripAntigravityRejectedClaudeSdkParagraph(systemText: string): string {
61
98
  return systemText
62
99
  .split("\n\n")
@@ -748,7 +785,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
748
785
  // AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
749
786
  const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
750
787
  const stripRejectedClaudeSdkParagraph = provider.googleMode === "cloud-code-assist"
751
- && parsed.modelId === "gemini-3.7-flash";
788
+ && rejectsClaudeSdkParagraph(parsed.modelId, routedModelId);
752
789
  const { systemInstruction, contents, replayedCallIds } = messagesToGeminiFormat(
753
790
  parsed,
754
791
  identityModelId,
@@ -22,11 +22,17 @@ export const CODEX_GPT5_IDENTITY_LINE = "You are Codex, a coding agent based on
22
22
  export const CODEX_GPT5_IDENTITY_LINE_AGENT = "You are Codex, an agent based on GPT-5.";
23
23
 
24
24
  /**
25
- * Known Codex GPT-5 identity sentences. Narrow: only "coding agent" / "an agent" + GPT-5(.x)?
25
+ * Known Codex identity sentences. Narrow: only "coding agent" / "an agent" + GPT-<major>(.minor)*.
26
26
  * Avoid a broad `You are Codex.*` rewrite that could touch unrelated content.
27
+ *
28
+ * The major version is a wildcard because Codex writes the CURRENT generation into this line and
29
+ * bumps it: `gpt-6-astra` (upstream #42607) ships "You are Codex, an agent based on GPT-6.".
30
+ * Pinning `GPT-5` meant a GPT-6-era prompt routed to a third-party provider kept telling that
31
+ * model it was Codex-on-GPT-6 — the exact misattribution this chokepoint exists to remove, silently
32
+ * reintroduced by a version bump.
27
33
  */
28
34
  const CODEX_GPT5_IDENTITY_RE =
29
- /You are Codex, (?:a coding agent|an agent) based on GPT-5(?:\.[0-9]+)*\./g;
35
+ /You are Codex, (?:a coding agent|an agent) based on GPT-[0-9]+(?:\.[0-9]+)*\./g;
30
36
 
31
37
  /** Proxy-neutral replacement: no "opencodex proxy" mention, just the GPT-5/OpenAI disclaimer. */
32
38
  export const NEUTRAL_IDENTITY_LINE = "You are a coding agent. Do not claim to be GPT-5 or to be made by OpenAI.";