@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.
- package/README.md +4 -0
- package/gui/dist/assets/index-B2YjLA-i.css +1 -0
- package/gui/dist/assets/{index-BHe2rl_C.js → index-aPup8CKb.js} +20 -20
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/meta.svg +1 -0
- package/package.json +4 -3
- package/src/adapters/cursor/catalog.ts +71 -29
- package/src/adapters/cursor/claude-id.ts +76 -0
- package/src/adapters/cursor/discovery.ts +16 -3
- package/src/adapters/cursor/effort-map.ts +27 -12
- package/src/adapters/google.ts +39 -2
- package/src/adapters/openai-responses.ts +14 -1
- package/src/cli/claude.ts +11 -2
- package/src/cli/connect.ts +7 -1
- package/src/cli/registry.ts +1 -1
- package/src/cli/status.ts +19 -4
- package/src/client/connect.ts +5 -1
- package/src/client/hub-client.ts +29 -5
- package/src/clients/config-export.ts +12 -2
- package/src/codex/catalog/aggregation.ts +8 -0
- package/src/codex/catalog/metadata.ts +5 -0
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-fetch.ts +163 -26
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/desired-state.ts +18 -11
- package/src/combos/failover.ts +185 -6
- package/src/combos/index.ts +6 -0
- package/src/combos/resolve.ts +43 -6
- package/src/config.ts +5 -1
- package/src/generated/compatibility-version.json +85 -61
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/sync.ts +10 -2
- package/src/integrations/cursor-effort-table.ts +143 -0
- package/src/integrations/state.ts +1 -1
- package/src/integrations/writer.ts +2 -2
- package/src/lib/app-owned-memory-stores.ts +27 -8
- package/src/lib/bounded-body.ts +16 -1
- package/src/oauth/generic-account-failover.ts +2 -2
- package/src/oauth/index.ts +11 -0
- package/src/oauth/meta-muse.ts +235 -0
- package/src/providers/antigravity-models.ts +71 -13
- package/src/providers/command-code-efforts.ts +15 -0
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/registry.ts +116 -8
- package/src/responses/code-mode-helper-compat.ts +4 -1
- package/src/responses/state.ts +5 -4
- package/src/server/auth-cors.ts +241 -56
- package/src/server/chat-completions.ts +11 -2
- package/src/server/chat-native.ts +30 -4
- package/src/server/claude-messages.ts +17 -3
- package/src/server/effort-row.ts +131 -0
- package/src/server/index.ts +67 -38
- package/src/server/management/api-key-rotation.ts +2 -1
- package/src/server/management/api-key-usage.ts +97 -43
- package/src/server/management/context.ts +3 -0
- package/src/server/management/cursor-integration-routes.ts +36 -7
- package/src/server/management/logs-usage-routes.ts +64 -87
- package/src/server/management/provider-routes.ts +218 -1
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/management/usage-aggregate-cache.ts +464 -0
- package/src/server/management/usage-summary-cache.ts +4 -0
- package/src/server/models-capabilities.ts +60 -5
- package/src/server/responses/core.ts +61 -7
- package/src/types/config.ts +10 -1
- package/src/types/tools.ts +12 -9
- package/src/usage/expected-prices.ts +43 -7
- package/src/usage/ledger-scanner.ts +448 -0
- package/src/usage/log.ts +1 -1
- package/src/usage/summary.ts +915 -655
- package/src/web-search/index.ts +1 -1
- package/gui/dist/assets/index-CJSb3HPe.css +0 -1
package/gui/dist/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
124
|
-
//
|
|
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(
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
package/src/adapters/google.ts
CHANGED
|
@@ -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
|
|
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"
|
|
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
|
-
|
|
358
|
-
|
|
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;
|
package/src/cli/connect.ts
CHANGED
|
@@ -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
|
}
|
package/src/cli/registry.ts
CHANGED
|
@@ -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:
|
|
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
|
|
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:
|
|
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:
|
|
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.
|
package/src/client/connect.ts
CHANGED
|
@@ -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, {
|
|
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
|
|