@bitkyc08/opencodex 2.40.0 → 2.41.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 (72) hide show
  1. package/README.md +4 -0
  2. package/gui/dist/assets/index-B2YjLA-i.css +1 -0
  3. package/gui/dist/assets/{index-BHe2rl_C.js → index-aPup8CKb.js} +20 -20
  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/google.ts +39 -2
  12. package/src/adapters/openai-responses.ts +14 -1
  13. package/src/cli/claude.ts +11 -2
  14. package/src/cli/connect.ts +7 -1
  15. package/src/cli/registry.ts +1 -1
  16. package/src/cli/status.ts +19 -4
  17. package/src/client/connect.ts +5 -1
  18. package/src/client/hub-client.ts +29 -5
  19. package/src/clients/config-export.ts +12 -2
  20. package/src/codex/catalog/aggregation.ts +8 -0
  21. package/src/codex/catalog/metadata.ts +5 -0
  22. package/src/codex/catalog/parsing.ts +2 -0
  23. package/src/codex/catalog/provider-fetch.ts +163 -26
  24. package/src/codex/catalog.ts +1 -1
  25. package/src/codex/convergence-types.ts +1 -0
  26. package/src/codex/desired-state.ts +18 -11
  27. package/src/combos/failover.ts +185 -6
  28. package/src/combos/index.ts +6 -0
  29. package/src/combos/resolve.ts +43 -6
  30. package/src/config.ts +5 -1
  31. package/src/generated/compatibility-version.json +85 -61
  32. package/src/generated/model-metadata.ts +1 -1
  33. package/src/grok/sync.ts +10 -2
  34. package/src/integrations/cursor-effort-table.ts +143 -0
  35. package/src/integrations/state.ts +1 -1
  36. package/src/integrations/writer.ts +2 -2
  37. package/src/lib/app-owned-memory-stores.ts +27 -8
  38. package/src/lib/bounded-body.ts +16 -1
  39. package/src/oauth/generic-account-failover.ts +2 -2
  40. package/src/oauth/index.ts +11 -0
  41. package/src/oauth/meta-muse.ts +235 -0
  42. package/src/providers/antigravity-models.ts +71 -13
  43. package/src/providers/command-code-efforts.ts +15 -0
  44. package/src/providers/free-directory.ts +4 -1
  45. package/src/providers/registry.ts +116 -8
  46. package/src/responses/code-mode-helper-compat.ts +4 -1
  47. package/src/responses/state.ts +5 -4
  48. package/src/server/auth-cors.ts +241 -56
  49. package/src/server/chat-completions.ts +11 -2
  50. package/src/server/chat-native.ts +30 -4
  51. package/src/server/claude-messages.ts +17 -3
  52. package/src/server/effort-row.ts +131 -0
  53. package/src/server/index.ts +67 -38
  54. package/src/server/management/api-key-rotation.ts +2 -1
  55. package/src/server/management/api-key-usage.ts +97 -43
  56. package/src/server/management/context.ts +3 -0
  57. package/src/server/management/cursor-integration-routes.ts +36 -7
  58. package/src/server/management/logs-usage-routes.ts +64 -87
  59. package/src/server/management/provider-routes.ts +218 -1
  60. package/src/server/management/route-registry.ts +1 -0
  61. package/src/server/management/usage-aggregate-cache.ts +464 -0
  62. package/src/server/management/usage-summary-cache.ts +4 -0
  63. package/src/server/models-capabilities.ts +60 -5
  64. package/src/server/responses/core.ts +61 -7
  65. package/src/types/config.ts +10 -1
  66. package/src/types/tools.ts +12 -9
  67. package/src/usage/expected-prices.ts +43 -7
  68. package/src/usage/ledger-scanner.ts +448 -0
  69. package/src/usage/log.ts +1 -1
  70. package/src/usage/summary.ts +915 -655
  71. package/src/web-search/index.ts +1 -1
  72. package/gui/dist/assets/index-CJSb3HPe.css +0 -1
@@ -16,8 +16,8 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-BHe2rl_C.js"></script>
20
- <link rel="stylesheet" crossorigin href="/assets/index-CJSb3HPe.css">
19
+ <script type="module" crossorigin src="/assets/index-aPup8CKb.js"></script>
20
+ <link rel="stylesheet" crossorigin href="/assets/index-B2YjLA-i.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 150 100" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient gradientUnits="userSpaceOnUse" id="meta-mark-a" x1="124.599" x2="159.703" y1="96.689" y2="57.25"><stop offset="0.21" stop-color="#0278F1"/><stop offset="0.533" stop-color="#0180FA"/></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="meta-mark-b" x1="43.859" x2="0" y1="4.984" y2="63.795"><stop offset="0.427" stop-color="#0165E0"/><stop offset="0.917" stop-color="#0180FA"/></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="meta-mark-c" x1="28.409" x2="134.567" y1="28.907" y2="71.769"><stop stop-color="#0064E0"/><stop offset="0.656" stop-color="#0066E2"/><stop offset="1" stop-color="#0278F1"/></linearGradient></defs><path d="M107.654 0c-12.3 0-21.915 9.264-30.618 21.032C65.076 5.802 55.073 0 43.103 0 18.698 0 0 31.76 0 65.376 0 86.41 10.177 99.679 27.223 99.679c12.268 0 21.092-5.784 36.778-33.203 0 0 6.539-11.547 11.037-19.501a514.1 514.1 0 0 1 4.98 8.227l7.356 12.374c14.329 23.977 22.312 32.103 36.778 32.103C140.758 99.68 150 86.23 150 64.757 150 29.56 130.88 0 107.654 0ZM52.039 59.051C39.322 78.987 34.922 83.455 27.841 83.455c-7.287 0-11.617-6.397-11.617-17.804 0-24.404 12.167-49.359 26.672-49.359 7.855 0 14.42 4.537 24.474 18.93-9.547 14.645-15.33 23.83-15.33 23.83Zm47.999-2.51-8.795-14.667c-2.38-3.87-4.66-7.428-6.862-10.689 7.927-12.234 14.465-18.33 22.241-18.33 16.155 0 29.079 23.786 29.079 53.002 0 11.137-3.647 17.599-11.205 17.599-7.244 0-10.704-4.785-24.458-26.914Z" fill="#0180FA"/><path d="M145.119 34.888h-14.878c3.44 8.718 5.46 19.42 5.46 30.969 0 11.137-3.647 17.599-11.205 17.599-1.403 0-2.663-.18-3.884-.63V99.5a33.48 33.48 0 0 0 3.54.18C140.758 99.68 150 86.23 150 64.756c0-10.702-1.768-20.882-4.881-29.87Z" fill="url(#meta-mark-a)"/><path d="M43.103 0c.253 0 .505.003.756.008v16.308c-.319-.016-.64-.024-.962-.024-14.138 0-26.055 23.706-26.65 47.503H.014C.588 30.77 19.08 0 43.103 0Z" fill="url(#meta-mark-b)"/><path d="M43.103 0c11.115 0 20.534 5.004 31.4 17.913 3.055 3.817 6.74 8.777 10.682 14.511l.017.025.024-.003c1.939 2.912 3.94 6.05 6.017 9.428l8.795 14.668c13.754 22.13 17.214 26.913 24.458 26.913h.103v16.22c-.148.003-.297.004-.447.004-14.466 0-22.449-8.126-36.778-32.103l-7.356-12.374a587.065 587.065 0 0 0-2.287-3.822l.019-.045c-5.483-8.971-8.971-14.454-11.962-18.44l-.05.037c-9.15-12.59-15.42-16.64-22.842-16.64h-.034V.001l.24-.001Z" fill="url(#meta-mark-c)"/></svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.40.0",
3
+ "version": "2.41.0",
4
4
  "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -74,9 +74,10 @@
74
74
  },
75
75
  "overrides": {
76
76
  "@hono/node-server": "2.1.0",
77
- "fast-uri": "^3.1.5",
77
+ "fast-uri": "^3.1.7",
78
78
  "hono": "4.13.1",
79
- "ip-address": "^10.4.0"
79
+ "ip-address": "^10.4.0",
80
+ "qs": "^6.16.0"
80
81
  },
81
82
  "keywords": [
82
83
  "codex",
@@ -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
@@ -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,
@@ -1965,6 +1965,18 @@ export function stripOpenAiOnlyWebSearchFields(body: unknown): unknown {
1965
1965
  return changed ? next : body;
1966
1966
  }
1967
1967
 
1968
+ /**
1969
+ * Muse Spark ids whose Responses gateway refuses `search_content_types` on a plain
1970
+ * `web_search` tool. Membership, not equality: 1.3 shipped 2026-09-02 as the
1971
+ * same-shaped successor to 1.2 on the same Zen wire, and an equality check would
1972
+ * have let a Codex-emitted `web_search` + `search_content_types` body reach the
1973
+ * gateway and come back 400 for every request the moment 1.3 was selected.
1974
+ */
1975
+ const MUSE_SPARK_WEB_SEARCH_STRICT_MODELS = new Set([
1976
+ "muse-spark-1.3-contributor",
1977
+ "muse-spark-1.2-contributor",
1978
+ ]);
1979
+
1968
1980
  /**
1969
1981
  * OpenCode Zen / Go Muse Spark Responses gateway refuses `search_content_types`
1970
1982
  * on a plain `web_search` tool (400) but accepts it on `web_search_preview`; a
@@ -1976,7 +1988,8 @@ export function stripOpenAiOnlyWebSearchFields(body: unknown): unknown {
1976
1988
  */
1977
1989
  function stripMuseSparkUnsupportedWebSearchFields(body: unknown, modelId: unknown): unknown {
1978
1990
  if (!isPlainObject(body)) return body;
1979
- if (typeof modelId !== "string" || modelId.trim().toLowerCase() !== "muse-spark-1.2-contributor") return body;
1991
+ if (typeof modelId !== "string") return body;
1992
+ if (!MUSE_SPARK_WEB_SEARCH_STRICT_MODELS.has(modelId.trim().toLowerCase())) return body;
1980
1993
 
1981
1994
  const rewriteTools = (tools: unknown[]): { tools: unknown[]; changed: boolean } => {
1982
1995
  let changed = false;
package/src/cli/claude.ts CHANGED
@@ -354,8 +354,17 @@ export function readConnectedClaudeContextWindows(path = DEFAULT_CATALOG_PATH):
354
354
  }
355
355
  }
356
356
 
357
- async function ensureProxyForClaude(): Promise<number | null> {
358
- const live = await findLiveProxy();
357
+ export type ClaudeProxyEnsureDeps = {
358
+ findLiveProxy?: typeof findLiveProxy;
359
+ };
360
+
361
+ export async function ensureProxyForClaude(deps: ClaudeProxyEnsureDeps = {}): Promise<number | null> {
362
+ // A proxy that has only just bound can miss a single probe while its event loop
363
+ // is still settling startup work — the same just-started race the stop paths
364
+ // already retry for (#764, SERVICE_STOP_LIVENESS). Only the attempts budget is
365
+ // borrowed here; the probe timeout remains DEFAULT_PROBE_TIMEOUT_MS (750 ms).
366
+ // Without this, `ocx claude` can spawn a second proxy while the first is serving.
367
+ const live = await (deps.findLiveProxy ?? findLiveProxy)({ attempts: 3 });
359
368
  if (live) return live.port;
360
369
  const cfgPort = loadConfig().port;
361
370
  const pinPort = typeof cfgPort === "number" && cfgPort > 0 ? cfgPort : 10100;
@@ -17,6 +17,7 @@ import {
17
17
  rejectArgs,
18
18
  runCliAction,
19
19
  takeFlag,
20
+ takeIntegerOption,
20
21
  takeOption,
21
22
  type RuntimeApiDeps,
22
23
  } from "./runtime-api";
@@ -25,7 +26,7 @@ export const CONNECT_USAGE = `Usage:
25
26
  ocx connect <url> [--management-url <url>]
26
27
  (--pairing-code-stdin | --admin-token-stdin)
27
28
  [--clients codex,claude] [--management-transport direct|relay]
28
- [--no-sync]
29
+ [--catalog-timeout <seconds>] [--no-sync]
29
30
  ocx connect status [--json]
30
31
  ocx connect rotate (--pairing-code-stdin | --admin-token-stdin)
31
32
  [--json]
@@ -147,6 +148,10 @@ async function runConnect(argv: string[], deps: RuntimeApiDeps): Promise<void> {
147
148
  if (!serverUrl || serverUrl.startsWith("--")) throw new CliUsageError("hub URL is required", CONNECT_USAGE);
148
149
  const managementUrl = takeOption(args, "--management-url");
149
150
  const clients = parseClients(takeOption(args, "--clients"));
151
+ const catalogTimeoutSeconds = takeIntegerOption(args, "--catalog-timeout", { min: 1 });
152
+ if (catalogTimeoutSeconds !== undefined && catalogTimeoutSeconds > 120) {
153
+ throw new CliUsageError("--catalog-timeout must be an integer between 1 and 120", CONNECT_USAGE);
154
+ }
150
155
  const managementTransport = takeOption(args, "--management-transport") ?? "direct";
151
156
  if (managementTransport !== "direct" && managementTransport !== "relay") {
152
157
  throw new CliUsageError("--management-transport must be direct or relay", CONNECT_USAGE);
@@ -167,6 +172,7 @@ async function runConnect(argv: string[], deps: RuntimeApiDeps): Promise<void> {
167
172
  selectedClients: clients,
168
173
  managementTransport,
169
174
  noSync,
175
+ ...(catalogTimeoutSeconds === undefined ? {} : { catalogTimeoutMs: catalogTimeoutSeconds * 1_000 }),
170
176
  }, { fetchImpl: deps.fetchImpl });
171
177
  console.log(`Connected to ${connection.serverUrl} as key ${connection.apiKeyId}.`);
172
178
  }
@@ -88,7 +88,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
88
88
  { name: "ensure", usage: "ocx ensure", summary: "Ensure the proxy is running and Codex config/cache are current." },
89
89
  {
90
90
  name: "connect",
91
- usage: "ocx connect <url> [--management-url <url>] (--pairing-code-stdin | --admin-token-stdin) [--clients codex,claude] [--management-transport direct|relay] [--no-sync]",
91
+ usage: "ocx connect <url> [--management-url <url>] (--pairing-code-stdin | --admin-token-stdin) [--clients codex,claude] [--management-transport direct|relay] [--catalog-timeout <seconds>] [--no-sync]",
92
92
  summary: "Connect this machine to a remote OpenCodex hub without persisting the one-time authority.",
93
93
  details: [
94
94
  "Status: ocx connect status [--json]",
package/src/cli/status.ts CHANGED
@@ -120,20 +120,35 @@ export type ListenTarget = {
120
120
  dashboardUrl: string;
121
121
  };
122
122
 
123
+ type StatusListenConfig = Pick<OcxConfig, "port" | "hostname" | "runtimeRole" | "hub">;
124
+
125
+ function statusDashboardUrl(config: StatusListenConfig, hostname: string | undefined, port: number): string {
126
+ const managementOrigin = config.runtimeRole === "hub" ? config.hub?.managementPublicOrigin : undefined;
127
+ if (managementOrigin) return managementOrigin.endsWith("/") ? managementOrigin : `${managementOrigin}/`;
128
+
129
+ const reachableHostname = probeHostname(hostname);
130
+ const dashboardHostname = reachableHostname === "127.0.0.1"
131
+ || reachableHostname === "[::1]"
132
+ || reachableHostname.toLowerCase() === "localhost"
133
+ ? "localhost"
134
+ : reachableHostname;
135
+ return `http://${dashboardHostname}:${port}/`;
136
+ }
137
+
123
138
  export function selectListenTarget(
124
- config: Pick<OcxConfig, "port" | "hostname">,
139
+ config: StatusListenConfig,
125
140
  pid: number | null,
126
141
  runtimePort: RuntimePortState | null,
127
142
  ): ListenTarget {
128
143
  const currentRuntimePort = pid && runtimePort?.pid === pid ? runtimePort : null;
129
144
  const port = currentRuntimePort ? currentRuntimePort.port : config.port ?? 10100;
130
- const hostname = currentRuntimePort ? currentRuntimePort.hostname : config.hostname;
145
+ const hostname = currentRuntimePort?.hostname ?? config.hostname;
131
146
  return {
132
147
  port,
133
148
  hostname,
134
149
  source: currentRuntimePort ? "runtime" : "config",
135
150
  healthUrl: `http://${probeHostname(hostname)}:${port}/healthz`,
136
- dashboardUrl: `http://localhost:${port}/`,
151
+ dashboardUrl: statusDashboardUrl(config, hostname, port),
137
152
  };
138
153
  }
139
154
 
@@ -341,7 +356,7 @@ export async function collectStatus(): Promise<CliStatusView> {
341
356
  hostname: live.hostname,
342
357
  source: live.source,
343
358
  healthUrl: `http://${probeHostname(live.hostname)}:${live.port}/healthz`,
344
- dashboardUrl: `http://localhost:${live.port}/`,
359
+ dashboardUrl: statusDashboardUrl(config, live.hostname, live.port),
345
360
  }
346
361
  : selectListenTarget(config, pidFile, pidFile ? readRuntimePort(pidFile) : null);
347
362
  // findLiveProxy already identity-probed /healthz; avoid a second fetch that can race.
@@ -71,6 +71,7 @@ export interface ConnectOptions {
71
71
  selectedClients: OcxConnectedClientId[];
72
72
  managementTransport: "direct" | "relay";
73
73
  noSync?: boolean;
74
+ catalogTimeoutMs?: number;
74
75
  }
75
76
 
76
77
  export interface ClientConnectDeps {
@@ -408,7 +409,10 @@ export async function connectClient(
408
409
  const persisted = writeServiceApiTokenFile(issued.key);
409
410
  tokenFingerprint = persisted.fingerprint;
410
411
 
411
- const catalog = await downloadClientCatalog(serverUrl, issued.key, { fetchImpl: deps.fetchImpl });
412
+ const catalog = await downloadClientCatalog(serverUrl, issued.key, {
413
+ fetchImpl: deps.fetchImpl,
414
+ timeoutMs: options.catalogTimeoutMs,
415
+ });
412
416
  atomicWriteFile(DEFAULT_CATALOG_PATH, catalog.body);
413
417
  writtenCatalogFingerprint = sha256(catalog.body);
414
418