@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.
- package/README.md +4 -0
- package/gui/dist/assets/index-BU1tE0sr.js +112 -0
- package/gui/dist/assets/index-DL9-iS6J.css +1 -0
- 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/cursor/protobuf-request.ts +41 -21
- package/src/adapters/google.ts +39 -2
- package/src/adapters/identity.ts +8 -2
- package/src/adapters/openai-responses.ts +57 -4
- package/src/bridge.ts +25 -3
- package/src/cli/account-auth.ts +28 -3
- package/src/cli/account-extended.ts +7 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/claude.ts +11 -2
- package/src/cli/connect.ts +7 -1
- package/src/cli/observe.ts +3 -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/auth-api.ts +102 -9
- package/src/codex/catalog/aggregation.ts +8 -0
- package/src/codex/catalog/effort.ts +15 -2
- package/src/codex/catalog/metadata.ts +119 -9
- package/src/codex/catalog/native-models.ts +71 -0
- package/src/codex/catalog/parsing.ts +5 -3
- package/src/codex/catalog/provider-fetch.ts +166 -28
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/data/upstream-models.json +169 -0
- package/src/codex/desired-state.ts +18 -11
- package/src/codex/inject.ts +96 -6
- package/src/codex/injected-marker.ts +30 -4
- package/src/codex/journal.ts +14 -0
- 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 +115 -83
- 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/account-quota-rank.ts +40 -1
- package/src/oauth/chatgpt-device.ts +187 -0
- package/src/oauth/chatgpt.ts +31 -4
- package/src/oauth/generic-account-failover.ts +2 -2
- package/src/oauth/index.ts +24 -3
- package/src/oauth/log.ts +3 -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/muse-subscription-usage.ts +95 -0
- package/src/providers/quota.ts +96 -0
- 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 +82 -45
- package/src/server/live.ts +18 -4
- 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/oauth-account-routes.ts +10 -3
- 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 +95 -7
- package/src/server/responses/empty-completion-guard.ts +4 -0
- package/src/types/config.ts +10 -1
- package/src/types/request.ts +8 -0
- 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-BHe2rl_C.js +0 -112
- 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
|
-
//
|
|
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
|
|
@@ -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
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (
|
|
318
|
-
const marked = `${normalized}\n[note: this exact output was produced ${
|
|
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(
|
|
325
|
-
|
|
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
|
-
|
|
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 &&
|
|
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.` }],
|
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,
|
package/src/adapters/identity.ts
CHANGED
|
@@ -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
|
|
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-
|
|
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.";
|