@bitkyc08/opencodex 2.6.32 → 2.7.1-preview.20260710
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.ko.md +9 -5
- package/README.md +7 -4
- package/README.zh-CN.md +8 -4
- package/gui/dist/assets/index-BUAMcKFd.css +1 -0
- package/gui/dist/assets/index-KorpEKW8.js +34 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +62 -1
- package/src/adapters/cursor/cursor-errors.ts +28 -1
- package/src/adapters/cursor/discovery.ts +60 -10
- package/src/adapters/cursor/effort-map.ts +38 -7
- package/src/adapters/cursor/live-models.ts +3 -0
- package/src/adapters/cursor/live-transport.ts +136 -7
- package/src/adapters/cursor/protobuf-request.ts +24 -1
- package/src/adapters/cursor/request-builder.ts +6 -5
- package/src/adapters/cursor/transport-retry.ts +22 -3
- package/src/adapters/cursor.ts +2 -1
- package/src/adapters/openai-chat.ts +75 -26
- package/src/bridge.ts +42 -3
- package/src/cli/debug.ts +203 -0
- package/src/cli/doctor.ts +11 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/v2.ts +131 -0
- package/src/codex/auth-api.ts +7 -3
- package/src/codex/catalog.ts +334 -31
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/features.ts +178 -0
- package/src/codex/project-config-warnings.ts +388 -0
- package/src/codex/sync.ts +8 -0
- package/src/codex/warmup.ts +62 -7
- package/src/config.ts +7 -5
- package/src/lib/debug-log-buffer.ts +42 -0
- package/src/lib/debug-settings.ts +84 -0
- package/src/lib/debug.ts +18 -9
- package/src/lib/errors.ts +104 -1
- package/src/oauth/cursor.ts +35 -12
- package/src/oauth/store.ts +4 -3
- package/src/providers/derive.ts +8 -0
- package/src/providers/registry.ts +56 -21
- package/src/reasoning-effort.ts +37 -9
- package/src/responses/parser.ts +7 -2
- package/src/router.ts +5 -0
- package/src/server/adapter-resolve.ts +1 -1
- package/src/server/index.ts +27 -3
- package/src/server/management-api.ts +189 -7
- package/src/server/relay.ts +2 -2
- package/src/server/request-decompress.ts +8 -2
- package/src/server/request-log.ts +78 -0
- package/src/server/responses.ts +241 -9
- package/src/types.ts +34 -1
- package/src/usage/debug.ts +32 -5
- package/src/usage/summary.ts +6 -6
- package/src/vision/describe.ts +4 -0
- package/src/web-search/executor.ts +4 -0
- package/src/web-search/format-result.ts +11 -3
- package/src/web-search/index.ts +31 -2
- package/src/web-search/loop.ts +112 -61
- package/src/web-search/parse.ts +4 -1
- package/gui/dist/assets/index-ByGC8-Bm.css +0 -1
- package/gui/dist/assets/index-D_JZzI0r.js +0 -15
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-KorpEKW8.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BUAMcKFd.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -482,11 +482,72 @@ function toolsToAnthropicFormat(parsed: OcxParsedRequest, toolNames: { toWire: (
|
|
|
482
482
|
const converted = tools.map(t => ({
|
|
483
483
|
name: toolNames.toWire(namespacedToolName(t.namespace, t.name)),
|
|
484
484
|
description: t.description,
|
|
485
|
-
input_schema: t.parameters,
|
|
485
|
+
input_schema: normalizeAnthropicInputSchema(t.parameters),
|
|
486
486
|
}));
|
|
487
487
|
return converted;
|
|
488
488
|
}
|
|
489
489
|
|
|
490
|
+
function normalizeAnthropicInputSchema(schema: unknown): Record<string, unknown> {
|
|
491
|
+
const obj = schema && typeof schema === "object" && !Array.isArray(schema)
|
|
492
|
+
? schema as Record<string, unknown>
|
|
493
|
+
: {};
|
|
494
|
+
// Anthropic rejects root-level missing type and oneOf/anyOf/allOf in input_schema.
|
|
495
|
+
// Normalize the root only: ensure type:"object" + properties, flatten root composition
|
|
496
|
+
// while preserving nested schemas. Mirrors kiro-tools.ts ensureRootObjectType.
|
|
497
|
+
// Known limitation: Object.assign on branch properties means later branches overwrite
|
|
498
|
+
// earlier ones when the same property name appears with different schemas.
|
|
499
|
+
const compositionKeys = ["oneOf", "anyOf", "allOf"] as const;
|
|
500
|
+
const hasRootComposition = compositionKeys.some(key => Array.isArray(obj[key]));
|
|
501
|
+
const type = obj.type;
|
|
502
|
+
const rootObjectType = type === "object" || (Array.isArray(type) && type.includes("object"));
|
|
503
|
+
|
|
504
|
+
if (!hasRootComposition) {
|
|
505
|
+
const normalized: Record<string, unknown> = rootObjectType && type === "object"
|
|
506
|
+
? { ...obj }
|
|
507
|
+
: { ...obj, type: "object" };
|
|
508
|
+
if (normalized.properties === undefined || normalized.properties === null) {
|
|
509
|
+
normalized.properties = {};
|
|
510
|
+
}
|
|
511
|
+
return normalized;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const properties: Record<string, unknown> = {};
|
|
515
|
+
const required = new Set<string>();
|
|
516
|
+
if (obj.properties && typeof obj.properties === "object" && !Array.isArray(obj.properties)) {
|
|
517
|
+
Object.assign(properties, obj.properties as Record<string, unknown>);
|
|
518
|
+
}
|
|
519
|
+
if (Array.isArray(obj.required)) {
|
|
520
|
+
for (const item of obj.required) if (typeof item === "string") required.add(item);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
for (const key of compositionKeys) {
|
|
524
|
+
const variants = obj[key];
|
|
525
|
+
if (!Array.isArray(variants)) continue;
|
|
526
|
+
const mergeRequired = key === "allOf";
|
|
527
|
+
for (const variant of variants) {
|
|
528
|
+
if (!variant || typeof variant !== "object" || Array.isArray(variant)) continue;
|
|
529
|
+
const v = variant as Record<string, unknown>;
|
|
530
|
+
if (v.properties && typeof v.properties === "object" && !Array.isArray(v.properties)) {
|
|
531
|
+
Object.assign(properties, v.properties as Record<string, unknown>);
|
|
532
|
+
}
|
|
533
|
+
if (mergeRequired && Array.isArray(v.required)) {
|
|
534
|
+
for (const item of v.required) if (typeof item === "string") required.add(item);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const normalized: Record<string, unknown> = {};
|
|
540
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
541
|
+
if (key === "oneOf" || key === "anyOf" || key === "allOf") continue;
|
|
542
|
+
if (key === "type" || key === "properties" || key === "required") continue;
|
|
543
|
+
normalized[key] = value;
|
|
544
|
+
}
|
|
545
|
+
normalized.type = "object";
|
|
546
|
+
normalized.properties = properties;
|
|
547
|
+
if (required.size > 0) normalized.required = [...required];
|
|
548
|
+
return normalized;
|
|
549
|
+
}
|
|
550
|
+
|
|
490
551
|
export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetention?: "none" | "short" | "long"): ProviderAdapter {
|
|
491
552
|
const isOAuth = provider.authMode === "oauth";
|
|
492
553
|
const toolNames = buildToolNameTransforms(provider);
|
|
@@ -11,6 +11,31 @@ function sanitize(value: string): string {
|
|
|
11
11
|
.replace(ABSOLUTE_PATH_PATTERN, "[REDACTED_PATH]");
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function errorMessage(value: unknown): string {
|
|
15
|
+
if (value instanceof Error) return value.message;
|
|
16
|
+
if (typeof value === "string") return value;
|
|
17
|
+
return String(value ?? "");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function errorCode(value: unknown): string {
|
|
21
|
+
if (typeof value !== "object" || !value || !("code" in value)) return "";
|
|
22
|
+
const code = (value as { code?: unknown }).code;
|
|
23
|
+
return code === undefined || code === null ? "" : String(code);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* True when Cursor intentionally cancelled the HTTP/2 stream after a client-tool suspend.
|
|
28
|
+
* These are expected between multi-turn Responses bridge cycles, not upstream failures.
|
|
29
|
+
*/
|
|
30
|
+
export function isCursorBenignCancelError(value: unknown): boolean {
|
|
31
|
+
const message = errorMessage(value).toLowerCase();
|
|
32
|
+
const code = errorCode(value).toUpperCase();
|
|
33
|
+
if (code === "NGHTTP2_CANCEL") return true;
|
|
34
|
+
if (message.includes("nghttp2_cancel")) return true;
|
|
35
|
+
if (message.includes("cursor stream suspended")) return true;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
14
39
|
/**
|
|
15
40
|
* Classify a Cursor transport/Connect/gRPC error message into an actionable category.
|
|
16
41
|
* The returned prefix string is recognized by `src/lib/errors.ts` `classifyError` keywords,
|
|
@@ -19,13 +44,15 @@ function sanitize(value: string): string {
|
|
|
19
44
|
export function classifyCursorError(message: string): string {
|
|
20
45
|
const lower = message.toLowerCase();
|
|
21
46
|
|
|
47
|
+
if (isCursorBenignCancelError(message)) return "Cursor stream suspended";
|
|
48
|
+
|
|
22
49
|
if (
|
|
23
50
|
lower.includes("resource_exhausted") ||
|
|
24
51
|
lower.includes("resource exhausted") ||
|
|
25
52
|
lower.includes("rate limit") ||
|
|
26
53
|
lower.includes("rate-limit") ||
|
|
27
54
|
lower.includes("too many requests") ||
|
|
28
|
-
lower.includes("
|
|
55
|
+
lower.includes("throttling")
|
|
29
56
|
) return "Cursor rate limit exceeded";
|
|
30
57
|
|
|
31
58
|
if (
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { cursorModelEffortLadder } from "./effort-map";
|
|
2
|
+
|
|
1
3
|
export interface CursorModelInfo {
|
|
2
4
|
id: string;
|
|
3
5
|
contextWindow?: number;
|
|
@@ -21,7 +23,9 @@ export function inferCursorContextWindow(modelId: string): number {
|
|
|
21
23
|
if (id.includes("1m")) return CONTEXT_1M;
|
|
22
24
|
if (id.startsWith("gemini-")) return CONTEXT_1M;
|
|
23
25
|
if (id === "glm-5.2") return CONTEXT_1M;
|
|
26
|
+
if (id.startsWith("gpt-5.6-")) return CONTEXT_1M;
|
|
24
27
|
if (id.startsWith("gpt-5") || id === "gpt-5-codex") return CONTEXT_272K;
|
|
28
|
+
if (id.startsWith("grok-4.5")) return 500_000;
|
|
25
29
|
if (id.startsWith("grok-")) return CONTEXT_256K;
|
|
26
30
|
if (id.includes("claude")) return CONTEXT_200K;
|
|
27
31
|
return CURSOR_DEFAULT_CONTEXT_WINDOW;
|
|
@@ -51,6 +55,42 @@ export function normalizeCursorModels(models: readonly CursorModelInfo[]): Curso
|
|
|
51
55
|
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
// Live GetUsableModels ids append effort tiers to the base id (`claude-4.6-opus-high`). Only
|
|
59
|
+
// these suffixes may activate a base model — otherwise a sibling model like `claude-4-sonnet-1m`
|
|
60
|
+
// would falsely activate `claude-4-sonnet` (PR #73 review finding).
|
|
61
|
+
const LIVE_EFFORT_SUFFIXES = ["low", "medium", "high", "xhigh", "max"] as const;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* True when a configured Cursor base model should remain exposed after live GetUsableModels filtering.
|
|
65
|
+
* Live ids are full effort-suffixed variants (`claude-4.6-opus-high`); base ids match exactly or by prefix.
|
|
66
|
+
*/
|
|
67
|
+
export function isCursorModelAvailableForAccount(modelId: string, liveIds: readonly string[]): boolean {
|
|
68
|
+
return liveIds.some(id =>
|
|
69
|
+
id === modelId || LIVE_EFFORT_SUFFIXES.some(suffix => id === `${modelId}-${suffix}`));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Codex-facing id for Cursor's auto-router. Always kept in the catalog even when live discovery omits it. */
|
|
73
|
+
export const CURSOR_AUTO_MODEL_ID = "auto";
|
|
74
|
+
|
|
75
|
+
/** Wire id Cursor Connect expects for the auto-router (GetUsableModels returns `default`, not `auto`). */
|
|
76
|
+
export const CURSOR_AUTO_WIRE_MODEL_ID = "default";
|
|
77
|
+
|
|
78
|
+
/** Map a Codex-facing Cursor model id to the upstream wire id. */
|
|
79
|
+
export function cursorCodexToWireModelId(modelId: string): string {
|
|
80
|
+
const normalized = modelId.startsWith("cursor/") ? modelId.slice("cursor/".length) : modelId;
|
|
81
|
+
return normalized === CURSOR_AUTO_MODEL_ID ? CURSOR_AUTO_WIRE_MODEL_ID : normalized;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Filter the static Cursor seed to models this account can use. */
|
|
85
|
+
export function filterCursorConfiguredModelsByLiveDiscovery<T extends { id: string }>(
|
|
86
|
+
configured: readonly T[],
|
|
87
|
+
liveIds: readonly string[],
|
|
88
|
+
): T[] {
|
|
89
|
+
return configured.filter(model =>
|
|
90
|
+
model.id === CURSOR_AUTO_MODEL_ID || isCursorModelAvailableForAccount(model.id, liveIds),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
54
94
|
export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorModels([
|
|
55
95
|
// Context windows and the model lineup mirror Cursor's public models/pricing docs plus the jawcode
|
|
56
96
|
// SOT (../jawcode/packages/ai/src/models.json, `cursor` provider), which mirrors the real
|
|
@@ -62,7 +102,7 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
|
|
|
62
102
|
// gemini/grok/kimi/gpt-5-mini are reasoning models in the SOT but are sent bare (no tier picker).
|
|
63
103
|
{ id: "auto", contextWindow: CONTEXT_200K, supportsReasoningEffort: false },
|
|
64
104
|
|
|
65
|
-
{ id: "claude-sonnet-5", contextWindow: CONTEXT_200K },
|
|
105
|
+
{ id: "claude-sonnet-5", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
66
106
|
{ id: "claude-4-sonnet", contextWindow: CONTEXT_200K },
|
|
67
107
|
{ id: "claude-4-sonnet-1m", contextWindow: CONTEXT_1M },
|
|
68
108
|
{ id: "claude-4.5-haiku", contextWindow: CONTEXT_200K },
|
|
@@ -71,12 +111,12 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
|
|
|
71
111
|
{ id: "claude-4.6-opus", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
72
112
|
{ id: "claude-4.6-sonnet", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
73
113
|
{ id: "claude-opus-4-7", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
114
|
+
// opus-4-7-fast: effort-suffix tiers unverified -> no tier picker; sent bare like live-only ids.
|
|
115
|
+
{ id: "claude-opus-4-7-fast", contextWindow: CONTEXT_200K },
|
|
74
116
|
{ id: "claude-opus-4-8", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
75
117
|
{ id: "claude-fable-5", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
76
118
|
|
|
77
119
|
{ id: "composer-1", contextWindow: CONTEXT_200K },
|
|
78
|
-
{ id: "composer-1.5", contextWindow: CONTEXT_200K },
|
|
79
|
-
{ id: "composer-2", contextWindow: CONTEXT_200K },
|
|
80
120
|
{ id: "composer-2.5", contextWindow: CONTEXT_200K },
|
|
81
121
|
{ id: "composer-2.5-fast", contextWindow: CONTEXT_200K },
|
|
82
122
|
|
|
@@ -101,15 +141,23 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
|
|
|
101
141
|
{ id: "gpt-5.4-mini", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
|
|
102
142
|
{ id: "gpt-5.4-nano", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
|
|
103
143
|
{ id: "gpt-5.5", contextWindow: CONTEXT_272K, supportsReasoningEffort: true },
|
|
144
|
+
// gpt-5.5-extra: absent from cursor.com docs but SURVIVES the live GetUsableModels filter
|
|
145
|
+
// (account-verified 260709, devlog/model_update/260709_model_refresh/004_live_snapshot.md).
|
|
104
146
|
{ id: "gpt-5.5-extra", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
147
|
+
{ id: "gpt-5.6-sol", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
148
|
+
{ id: "gpt-5.6-terra", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
149
|
+
{ id: "gpt-5.6-luna", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
150
|
+
|
|
151
|
+
// 260709 refresh: stale grok/composer/kimi/gpt ids dropped per current cursor.com docs; the
|
|
152
|
+
// 260709 note: grok-4.5 was deferred; confirmed live 260708 (cursor.com/models, xAI launch).
|
|
105
153
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
{ id: "
|
|
109
|
-
{ id: "
|
|
154
|
+
// Conflict resolution (260709): keep the refreshed 1M context + kimi-k2.7-code from de12fc8,
|
|
155
|
+
// take PR #73's supportsReasoningEffort for glm-5.2 (its effort-map tiers landed with the PR).
|
|
156
|
+
{ id: "glm-5.2", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
157
|
+
{ id: "kimi-k2.7-code", contextWindow: CONTEXT_262K },
|
|
110
158
|
|
|
111
|
-
{ id: "
|
|
112
|
-
{ id: "
|
|
159
|
+
{ id: "grok-4.5", contextWindow: 500_000, supportsReasoningEffort: true },
|
|
160
|
+
{ id: "grok-4.5-fast", contextWindow: 500_000 },
|
|
113
161
|
]);
|
|
114
162
|
|
|
115
163
|
export function cursorModelIds(models: readonly CursorModelInfo[] = CURSOR_STATIC_MODELS): string[] {
|
|
@@ -138,7 +186,9 @@ export function cursorModelReasoningEfforts(
|
|
|
138
186
|
return Object.fromEntries(
|
|
139
187
|
normalizeCursorModels(models).map(model => [
|
|
140
188
|
model.id,
|
|
141
|
-
model.supportsReasoningEffort === true
|
|
189
|
+
model.supportsReasoningEffort === true
|
|
190
|
+
? cursorModelEffortLadder(model.id) ?? [...CURSOR_REASONING_EFFORTS]
|
|
191
|
+
: [],
|
|
142
192
|
]),
|
|
143
193
|
);
|
|
144
194
|
}
|
|
@@ -6,19 +6,30 @@
|
|
|
6
6
|
* `claude-4.6-sonnet` only has `-medium`, and `composer`/`grok`/`gemini` take no suffix at all. A bare
|
|
7
7
|
* id for a model that requires a suffix is rejected `ERROR_BAD_MODEL_NAME` (devlog 350.105).
|
|
8
8
|
*
|
|
9
|
+
* Canonical effort order is always low < medium < high < xhigh < max (max is the top tier, confirmed
|
|
10
|
+
* against Anthropic docs and Cursor's live lineup). Tiers are stored in ascending canonical order.
|
|
11
|
+
*
|
|
9
12
|
* `CURSOR_MODEL_EFFORT_TIERS` is the real catalog (from the Cursor `GetUsableModels` naming, mirrored in
|
|
10
13
|
* jawcode's bundle), each base model -> its available suffixes in ascending order. `cursorEffortSuffix`
|
|
11
|
-
*
|
|
12
|
-
*
|
|
14
|
+
* is literal-first: when the requested effort is one of the model's tiers, the effort you name is the
|
|
15
|
+
* suffix Cursor receives. It only clamps Codex effort ranks for efforts outside that model's tier set.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
|
|
16
19
|
"claude-4.5-opus": ["high"],
|
|
17
20
|
"claude-4.6-opus": ["high", "max"],
|
|
18
21
|
"claude-4.6-sonnet": ["medium"],
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"claude-
|
|
22
|
+
// max is always the top tier (canonical order: low < medium < high < xhigh < max), confirmed
|
|
23
|
+
// against Anthropic's effort ladder docs and Cursor's live model lineup.
|
|
24
|
+
"claude-fable-5": ["low", "medium", "high", "xhigh", "max"],
|
|
25
|
+
"claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"],
|
|
26
|
+
"claude-opus-4-8": ["low", "medium", "high", "xhigh", "max"],
|
|
27
|
+
"claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
|
|
28
|
+
"glm-5.2": ["high", "max"],
|
|
29
|
+
// GetUsableModels (2026-07-09) lists grok-4.5-{medium,high,xhigh} and grok-4.5-fast-{medium,high,xhigh};
|
|
30
|
+
// the bare "grok-4.5-fast" id was removed upstream and now returns not_found.
|
|
31
|
+
"grok-4.5": ["medium", "high", "xhigh"],
|
|
32
|
+
"grok-4.5-fast": ["medium", "high", "xhigh"],
|
|
22
33
|
"gpt-5.1": ["low", "high"],
|
|
23
34
|
"gpt-5.1-codex-max": ["low", "medium", "high", "xhigh"],
|
|
24
35
|
"gpt-5.1-codex-mini": ["low", "high"],
|
|
@@ -30,11 +41,21 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
|
|
|
30
41
|
"gpt-5.4-nano": ["low", "medium", "high", "xhigh"],
|
|
31
42
|
"gpt-5.5": ["low", "medium", "high"],
|
|
32
43
|
"gpt-5.5-extra": ["high"],
|
|
44
|
+
"gpt-5.6-sol": ["low", "medium", "high", "xhigh", "max"],
|
|
45
|
+
"gpt-5.6-terra": ["low", "medium", "high", "xhigh", "max"],
|
|
46
|
+
"gpt-5.6-luna": ["low", "medium", "high", "xhigh", "max"],
|
|
33
47
|
};
|
|
34
48
|
|
|
49
|
+
const CANONICAL_CODEX_EFFORT_ORDER = ["low", "medium", "high", "xhigh", "max"] as const;
|
|
50
|
+
|
|
51
|
+
function normalizeRequestedEffort(reasoning: string | undefined): string | undefined {
|
|
52
|
+
const normalized = reasoning?.toLowerCase();
|
|
53
|
+
return normalized === "ultra" ? "max" : normalized;
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
/** Collapse a Codex reasoning-effort label to a low/medium/high rank for clamping onto a model's tiers. */
|
|
36
57
|
function codexEffortRank(reasoning: string | undefined): "low" | "medium" | "high" {
|
|
37
|
-
switch ((reasoning ?? "")
|
|
58
|
+
switch (normalizeRequestedEffort(reasoning) ?? "") {
|
|
38
59
|
case "none":
|
|
39
60
|
case "minimal":
|
|
40
61
|
case "low":
|
|
@@ -53,11 +74,13 @@ function codexEffortRank(reasoning: string | undefined): "low" | "medium" | "hig
|
|
|
53
74
|
|
|
54
75
|
/**
|
|
55
76
|
* The Cursor effort suffix to use for `baseModelId` given a Codex reasoning effort, or `undefined` when
|
|
56
|
-
* the model takes no suffix (bare).
|
|
77
|
+
* the model takes no suffix (bare). Literal model tiers pass through; unknown efforts clamp by rank.
|
|
57
78
|
*/
|
|
58
79
|
export function cursorEffortSuffix(baseModelId: string, reasoning: string | undefined): string | undefined {
|
|
59
80
|
const tiers = CURSOR_MODEL_EFFORT_TIERS[baseModelId];
|
|
60
81
|
if (!tiers || tiers.length === 0) return undefined;
|
|
82
|
+
const requested = normalizeRequestedEffort(reasoning);
|
|
83
|
+
if (requested && tiers.includes(requested)) return requested;
|
|
61
84
|
switch (codexEffortRank(reasoning)) {
|
|
62
85
|
case "low":
|
|
63
86
|
return tiers[0];
|
|
@@ -68,6 +91,14 @@ export function cursorEffortSuffix(baseModelId: string, reasoning: string | unde
|
|
|
68
91
|
}
|
|
69
92
|
}
|
|
70
93
|
|
|
94
|
+
/** The Codex-facing picker ladder for a Cursor model, sorted in canonical Codex effort order. */
|
|
95
|
+
export function cursorModelEffortLadder(baseModelId: string): string[] | undefined {
|
|
96
|
+
const tiers = CURSOR_MODEL_EFFORT_TIERS[baseModelId];
|
|
97
|
+
if (!tiers || tiers.length === 0) return undefined;
|
|
98
|
+
const tierSet = new Set(tiers);
|
|
99
|
+
return CANONICAL_CODEX_EFFORT_ORDER.filter(effort => tierSet.has(effort));
|
|
100
|
+
}
|
|
101
|
+
|
|
71
102
|
/** Base models known to carry a reasoning-effort suffix (everything else is sent bare). */
|
|
72
103
|
export function cursorModelHasEffortTiers(baseModelId: string): boolean {
|
|
73
104
|
return (CURSOR_MODEL_EFFORT_TIERS[baseModelId]?.length ?? 0) > 0;
|
|
@@ -66,6 +66,7 @@ export async function fetchCursorUsableModels(opts: CursorUsableModelsOptions):
|
|
|
66
66
|
"x-ghost-mode": "true",
|
|
67
67
|
"x-cursor-client-version": opts.clientVersion ?? CURSOR_DISCOVERY_CLIENT_VERSION,
|
|
68
68
|
"x-cursor-client-type": "cli",
|
|
69
|
+
"x-session-id": crypto.randomUUID(),
|
|
69
70
|
});
|
|
70
71
|
|
|
71
72
|
let status = 0;
|
|
@@ -79,6 +80,8 @@ export async function fetchCursorUsableModels(opts: CursorUsableModelsOptions):
|
|
|
79
80
|
if (status !== 200) return close(null);
|
|
80
81
|
try {
|
|
81
82
|
const response = fromBinary(GetUsableModelsResponseSchema, new Uint8Array(Buffer.concat(chunks)));
|
|
83
|
+
// Account filtering uses wire `model_id` values only. Aliases like `composer-2-5` must not
|
|
84
|
+
// make stale configured ids such as `composer-2` look activated.
|
|
82
85
|
const ids = (response.models ?? [])
|
|
83
86
|
.map(model => (model as { modelId?: string }).modelId)
|
|
84
87
|
.filter((id): id is string => typeof id === "string" && id.length > 0);
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
type InteractionResponse,
|
|
32
32
|
} from "./gen/agent_pb";
|
|
33
33
|
import { debugProviderDiagnostic } from "../../lib/debug";
|
|
34
|
+
import { classifyCursorError, isCursorBenignCancelError, safeCursorErrorMessage } from "./cursor-errors";
|
|
34
35
|
import { mcpArgsFromToolCall } from "./protobuf-events";
|
|
35
36
|
import { OCX_RESPONSES_TOOL_PROVIDER } from "./tool-definitions";
|
|
36
37
|
import { cursorUnsafeNativeLocalExecEnabled, handleCursorNativeExec, handleCursorNativeKv, type CursorNativeExecContext } from "./native-exec";
|
|
@@ -53,7 +54,7 @@ import type { CursorClientMessage, CursorRunRequest, CursorServerMessage } from
|
|
|
53
54
|
import type { CursorTransport, CursorTransportFactoryInput } from "./transport";
|
|
54
55
|
|
|
55
56
|
const CURSOR_RUN_PATH = "/agent.v1.AgentService/Run";
|
|
56
|
-
const CURSOR_CLIENT_VERSION = "cli-2026.
|
|
57
|
+
const CURSOR_CLIENT_VERSION = "cli-2026.07.08-0c04a8a";
|
|
57
58
|
const HEARTBEAT_MS = 5_000;
|
|
58
59
|
const CURSOR_FIRST_FRAME_TIMEOUT_MS = 30_000;
|
|
59
60
|
const CLIENT_TOOL_FINALIZE_GRACE_MS = 50;
|
|
@@ -323,6 +324,14 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
323
324
|
private readonly desktopDeps: CursorNativeToolDeps;
|
|
324
325
|
private execContext: CursorNativeExecContext = {};
|
|
325
326
|
private mcpPrepared?: Promise<void>;
|
|
327
|
+
// Per-turn diagnostic counters/timestamps when provider debug is on (`ocx debug provider on`). Stamped in open(), cleared on
|
|
328
|
+
// close; safe to read after a stream failure because open() owns the only writer before run().
|
|
329
|
+
private turnStartedAt = 0;
|
|
330
|
+
private framesReceived = 0;
|
|
331
|
+
private firstFrameAt?: number;
|
|
332
|
+
private firstFrameLogged = false;
|
|
333
|
+
/** Stable session identifier sent as x-session-id; mirrors IDE session semantics. */
|
|
334
|
+
private readonly sessionId = crypto.randomUUID();
|
|
326
335
|
|
|
327
336
|
constructor(private readonly input: CursorTransportFactoryInput) {
|
|
328
337
|
this.token = resolveCursorToken(input.provider, input.headers);
|
|
@@ -379,6 +388,27 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
379
388
|
let done = false;
|
|
380
389
|
let failure: Error | undefined;
|
|
381
390
|
let state = createCursorProtobufEventState();
|
|
391
|
+
let failureLogged = false;
|
|
392
|
+
// One per-turn summary of the failure path (end-stream error, socket reset, abort) so the
|
|
393
|
+
// operator can see how far the turn got and how it was classified without re-scanning every
|
|
394
|
+
// frame. Gated behind provider debug (`ocx debug provider on`).
|
|
395
|
+
const summarizeFailure = (err: Error): Error => {
|
|
396
|
+
if (!failureLogged && !(this.expectedClose && isCursorBenignCancelError(err))) {
|
|
397
|
+
failureLogged = true;
|
|
398
|
+
debugProviderDiagnostic("cursor", "turn-failed", {
|
|
399
|
+
committed: this.committed,
|
|
400
|
+
framesReceived: this.framesReceived,
|
|
401
|
+
outputTokens: state.usage.outputTokens,
|
|
402
|
+
contextTokens: state.contextTokens,
|
|
403
|
+
firstFrameMs: this.firstFrameAt ? this.firstFrameAt - this.turnStartedAt : undefined,
|
|
404
|
+
elapsedMs: this.turnStartedAt ? Date.now() - this.turnStartedAt : undefined,
|
|
405
|
+
classified: classifyCursorError(err.message),
|
|
406
|
+
errorCode: (err as { code?: unknown }).code ?? undefined,
|
|
407
|
+
message: redactCursorForLog(err.message),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
return err;
|
|
411
|
+
};
|
|
382
412
|
const wake = () => {
|
|
383
413
|
const fn = notify;
|
|
384
414
|
notify = undefined;
|
|
@@ -428,13 +458,21 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
428
458
|
const message = queue.shift();
|
|
429
459
|
if (message) yield message;
|
|
430
460
|
}
|
|
431
|
-
if (failure)
|
|
461
|
+
if (failure) {
|
|
462
|
+
// A CANCEL is benign only on the client-tool suspend path (expectedClose); an
|
|
463
|
+
// unexpected server-side NGHTTP2_CANCEL must surface as a real transport error.
|
|
464
|
+
if (this.expectedClose && isCursorBenignCancelError(failure)) return;
|
|
465
|
+
throw attachPartialUsage(summarizeFailure(failure), state);
|
|
466
|
+
}
|
|
432
467
|
if (done) break;
|
|
433
468
|
await new Promise<void>(resolve => {
|
|
434
469
|
notify = resolve;
|
|
435
470
|
});
|
|
436
471
|
}
|
|
437
|
-
if (failure)
|
|
472
|
+
if (failure) {
|
|
473
|
+
if (this.expectedClose && isCursorBenignCancelError(failure)) return;
|
|
474
|
+
throw attachPartialUsage(summarizeFailure(failure), state);
|
|
475
|
+
}
|
|
438
476
|
}
|
|
439
477
|
|
|
440
478
|
writeClient(_message: CursorClientMessage): void {}
|
|
@@ -506,6 +544,11 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
506
544
|
const terminal = finalizeAfterDrain(state);
|
|
507
545
|
if (terminal.length === 0) return;
|
|
508
546
|
for (const event of terminal) push(event);
|
|
547
|
+
debugProviderDiagnostic("cursor", "client-tool-suspend", {
|
|
548
|
+
reason: "Responses bridge owns client tools; ending turn without fake mcpResult",
|
|
549
|
+
framesReceived: this.framesReceived,
|
|
550
|
+
elapsedMs: Date.now() - this.turnStartedAt,
|
|
551
|
+
});
|
|
509
552
|
this.cancelCursorRun();
|
|
510
553
|
}, this.activeClientToolFinalizeGraceMs);
|
|
511
554
|
}
|
|
@@ -518,11 +561,20 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
518
561
|
fail: (error: Error) => void,
|
|
519
562
|
finish: () => void,
|
|
520
563
|
): void {
|
|
564
|
+
this.turnStartedAt = Date.now();
|
|
565
|
+
this.framesReceived = 0;
|
|
566
|
+
this.firstFrameAt = undefined;
|
|
567
|
+
this.firstFrameLogged = false;
|
|
568
|
+
const dialHost = cursorHostLabel(this.input.provider.baseUrl || "https://api2.cursor.sh");
|
|
569
|
+
debugProviderDiagnostic("cursor", "dial", { host: dialHost });
|
|
521
570
|
this.session = http2.connect(this.input.provider.baseUrl || "https://api2.cursor.sh");
|
|
522
571
|
// The run request is buffered until the HTTP/2 session connects. Failures before `connect`
|
|
523
572
|
// (DNS, ECONNREFUSED, TLS, connect timeout) mean the server never received the request, so they
|
|
524
573
|
// are safe to retry. Once connected, bytes flush to the server and the turn must not be replayed.
|
|
525
|
-
this.session.on("connect", () => {
|
|
574
|
+
this.session.on("connect", () => {
|
|
575
|
+
this.committed = true;
|
|
576
|
+
debugProviderDiagnostic("cursor", "connected", { connectMs: Date.now() - this.turnStartedAt });
|
|
577
|
+
});
|
|
526
578
|
this.stream = this.session.request({
|
|
527
579
|
":method": "POST",
|
|
528
580
|
":path": CURSOR_RUN_PATH,
|
|
@@ -534,6 +586,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
534
586
|
"x-cursor-client-version": CURSOR_CLIENT_VERSION,
|
|
535
587
|
"x-cursor-client-type": "cli",
|
|
536
588
|
"x-request-id": crypto.randomUUID(),
|
|
589
|
+
"x-session-id": this.sessionId,
|
|
537
590
|
});
|
|
538
591
|
|
|
539
592
|
// Single owner of the pre-first-frame deadline. Cleared by the first server frame/end-stream and
|
|
@@ -543,6 +596,12 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
543
596
|
if (this.expectedClose) {
|
|
544
597
|
// We already emitted a terminal `done` and cancelled the run (client-tool suspension). The
|
|
545
598
|
// RST_STREAM CANCEL surfaces here as a stream error/abort; it is expected, not a failure.
|
|
599
|
+
debugProviderDiagnostic("cursor", "stream-cancel-expected", {
|
|
600
|
+
code: (error as { code?: unknown }).code,
|
|
601
|
+
message: redactCursorForLog(error.message),
|
|
602
|
+
framesReceived: this.framesReceived,
|
|
603
|
+
elapsedMs: Date.now() - this.turnStartedAt,
|
|
604
|
+
});
|
|
546
605
|
finish();
|
|
547
606
|
return;
|
|
548
607
|
}
|
|
@@ -552,6 +611,7 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
552
611
|
const stream = this.stream;
|
|
553
612
|
this.firstFrameTimer = setTimeout(() => {
|
|
554
613
|
this.firstFrameTimer = undefined;
|
|
614
|
+
debugProviderDiagnostic("cursor", "first-frame-timeout", { timeoutMs: this.input.firstFrameTimeoutMs ?? CURSOR_FIRST_FRAME_TIMEOUT_MS });
|
|
555
615
|
try { stream.close(); } catch { /* already closing */ }
|
|
556
616
|
try { session.close(); } catch { /* already closing */ }
|
|
557
617
|
fail(new Error("Cursor transport timed out before first response"));
|
|
@@ -560,6 +620,11 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
560
620
|
let pending: Uint8Array<ArrayBufferLike> = new Uint8Array();
|
|
561
621
|
this.stream.on("data", chunk => {
|
|
562
622
|
this.clearFirstFrameTimer();
|
|
623
|
+
if (!this.firstFrameLogged) {
|
|
624
|
+
this.firstFrameLogged = true;
|
|
625
|
+
this.firstFrameAt = Date.now();
|
|
626
|
+
debugProviderDiagnostic("cursor", "first-frame", { latencyMs: this.firstFrameAt - this.turnStartedAt });
|
|
627
|
+
}
|
|
563
628
|
const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
564
629
|
pending = concatBytes(pending, bytes);
|
|
565
630
|
try {
|
|
@@ -567,8 +632,16 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
567
632
|
pending = decoded.remainder;
|
|
568
633
|
const frames = decoded.frames;
|
|
569
634
|
for (const frame of frames) {
|
|
635
|
+
this.framesReceived++;
|
|
570
636
|
if ((frame.flags & CONNECT_FLAG_END_STREAM) === CONNECT_FLAG_END_STREAM) {
|
|
571
637
|
const endError = parseConnectEndStreamError(frame.payload);
|
|
638
|
+
debugProviderDiagnostic("cursor", "connect-end-stream", endError ? {
|
|
639
|
+
code: cursorConnectErrorCode(frame.payload),
|
|
640
|
+
message: redactCursorForLog(endError.message),
|
|
641
|
+
classified: classifyCursorError(endError.message),
|
|
642
|
+
framesReceived: this.framesReceived,
|
|
643
|
+
elapsedMs: Date.now() - this.turnStartedAt,
|
|
644
|
+
} : { framesReceived: this.framesReceived, elapsedMs: Date.now() - this.turnStartedAt });
|
|
572
645
|
if (endError) failAndClear(endError);
|
|
573
646
|
continue;
|
|
574
647
|
}
|
|
@@ -582,10 +655,38 @@ class LiveCursorTransport implements CursorTransport {
|
|
|
582
655
|
});
|
|
583
656
|
this.stream.on("trailers", trailers => {
|
|
584
657
|
const status = trailers["grpc-status"];
|
|
658
|
+
if (status !== undefined) debugProviderDiagnostic("cursor", "trailers", { grpcStatus: String(status) });
|
|
585
659
|
if (status && status !== "0") failAndClear(new Error(`Cursor gRPC error ${status}`));
|
|
586
660
|
});
|
|
587
|
-
this.stream.on("error", err =>
|
|
588
|
-
|
|
661
|
+
this.stream.on("error", err => {
|
|
662
|
+
const realErr = err instanceof Error ? err : new Error(String(err));
|
|
663
|
+
if (this.expectedClose) {
|
|
664
|
+
failAndClear(realErr);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
const code = (realErr as { code?: unknown }).code;
|
|
668
|
+
const errno = (realErr as { errno?: unknown }).errno;
|
|
669
|
+
debugProviderDiagnostic("cursor", "stream-error", {
|
|
670
|
+
code: typeof code === "string" || typeof code === "number" ? String(code) : undefined,
|
|
671
|
+
errno: typeof errno === "string" || typeof errno === "number" ? String(errno) : undefined,
|
|
672
|
+
name: realErr.name,
|
|
673
|
+
message: redactCursorForLog(realErr.message),
|
|
674
|
+
committed: this.committed,
|
|
675
|
+
framesReceived: this.framesReceived,
|
|
676
|
+
elapsedMs: Date.now() - this.turnStartedAt,
|
|
677
|
+
});
|
|
678
|
+
failAndClear(realErr);
|
|
679
|
+
});
|
|
680
|
+
this.stream.on("end", () => {
|
|
681
|
+
this.clearFirstFrameTimer();
|
|
682
|
+
debugProviderDiagnostic("cursor", "stream-end", {
|
|
683
|
+
committed: this.committed,
|
|
684
|
+
framesReceived: this.framesReceived,
|
|
685
|
+
expectedClose: this.expectedClose,
|
|
686
|
+
elapsedMs: Date.now() - this.turnStartedAt,
|
|
687
|
+
});
|
|
688
|
+
finish();
|
|
689
|
+
});
|
|
589
690
|
|
|
590
691
|
signal?.addEventListener("abort", () => {
|
|
591
692
|
this.close();
|
|
@@ -689,7 +790,7 @@ function attachPartialUsage(failure: Error, state: ReturnType<typeof createCurso
|
|
|
689
790
|
}
|
|
690
791
|
|
|
691
792
|
/**
|
|
692
|
-
* Compact frame descriptor for
|
|
793
|
+
* Compact frame descriptor for provider debug (`ocx debug provider on`): outer case plus the inner
|
|
693
794
|
* interactionUpdate/exec case and tool-call union case when present. No payload content is logged.
|
|
694
795
|
*/
|
|
695
796
|
function describeCursorServerFrame(message: AgentServerMessage): Record<string, unknown> {
|
|
@@ -758,6 +859,34 @@ function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
|
758
859
|
return out;
|
|
759
860
|
}
|
|
760
861
|
|
|
862
|
+
/** Host-only label for Cursor transport diagnostics — never leaks path/query/credentials. */
|
|
863
|
+
function cursorHostLabel(baseUrl: string): string {
|
|
864
|
+
try {
|
|
865
|
+
return new URL(baseUrl).host;
|
|
866
|
+
} catch {
|
|
867
|
+
return "cursor";
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/** Redact a Cursor error message for diagnostic output. Cursor error strings can carry raw
|
|
872
|
+
* credential key=value pairs beyond what redactSecretString covers; safeCursorErrorMessage
|
|
873
|
+
* already applies the full sanitizer plus the classified prefix, so reuse it verbatim. */
|
|
874
|
+
function redactCursorForLog(message: string): string {
|
|
875
|
+
return safeCursorErrorMessage(message).slice(0, 300);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/** Extract the Connect end-stream `error.code` from the raw trailer frame payload without
|
|
879
|
+
* surfacing the (potentially secret-bearing) message — used for `[ocx:cursor:connect-end-stream]`
|
|
880
|
+
* diagnostics. Returns undefined when the payload is not the expected Connect error shape. */
|
|
881
|
+
function cursorConnectErrorCode(payload: Uint8Array): string | undefined {
|
|
882
|
+
try {
|
|
883
|
+
const parsed = JSON.parse(new TextDecoder().decode(payload)) as { error?: { code?: string } };
|
|
884
|
+
return parsed?.error?.code;
|
|
885
|
+
} catch {
|
|
886
|
+
return undefined;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
761
890
|
export function createLiveCursorTransport(input: CursorTransportFactoryInput): CursorTransport {
|
|
762
891
|
return new LiveCursorTransport(input);
|
|
763
892
|
}
|