@yansigit/opencodex 2.33.0 → 2.35.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 +3 -3
- package/gui/dist/assets/index-BjCaHxdz.js +112 -0
- package/gui/dist/assets/index-DLkXOXLC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +141 -23
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/discovery.ts +60 -2
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +17 -1
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-fs.ts +13 -12
- package/src/adapters/cursor/native-exec-network.ts +3 -5
- package/src/adapters/cursor/native-exec-policy.ts +47 -0
- package/src/adapters/cursor/native-exec-shell.ts +116 -31
- package/src/adapters/cursor/native-exec.ts +38 -10
- package/src/adapters/cursor/protobuf-events.ts +28 -2
- package/src/adapters/cursor/protobuf-request.ts +93 -41
- package/src/adapters/cursor/request-builder.ts +39 -10
- package/src/adapters/cursor/tool-definitions.ts +27 -3
- package/src/adapters/cursor/tool-result-normalize.ts +51 -6
- package/src/adapters/cursor/types.ts +23 -4
- package/src/adapters/cursor.ts +170 -29
- package/src/adapters/google-aistudio-parser.ts +49 -0
- package/src/adapters/google-antigravity-replay.ts +105 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +41 -12
- package/src/adapters/google-http.ts +12 -11
- package/src/adapters/google.ts +219 -36
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +15 -0
- package/src/adapters/kiro-tools.ts +43 -15
- package/src/adapters/kiro.ts +54 -9
- package/src/adapters/openai-chat.ts +286 -242
- package/src/adapters/openai-responses.ts +335 -24
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -26
- package/src/chat/inbound.ts +29 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/outbound.ts +10 -8
- package/src/cli/account-api.ts +27 -7
- package/src/cli/account-extended.ts +10 -3
- package/src/cli/account.ts +29 -5
- package/src/cli/alias.ts +66 -0
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +6 -1
- package/src/cli/init.ts +1 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +13 -7
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/registry.ts +6 -1
- package/src/cli/telemetry-commands.ts +25 -0
- package/src/cli/v2.ts +34 -10
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +80 -6
- package/src/codex/auth-api.ts +48 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/model-metadata.ts +13 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +16 -0
- package/src/codex/catalog/provider-fetch.ts +20 -3
- package/src/codex/catalog/sync.ts +127 -2
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +22 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +18 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +14 -1
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +123 -18
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +198 -27
- package/src/codex/transition-state.ts +107 -8
- package/src/combos/types.ts +10 -0
- package/src/compatibility/openai-responses.ts +33 -1
- package/src/config/autonomous-remediation.ts +21 -0
- package/src/config/provider-validation.ts +14 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +191 -17
- package/src/generated/compatibility-version.json +279 -159
- package/src/generated/model-metadata.ts +3 -0
- package/src/images/loop.ts +5 -4
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +11 -2
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/oauth/aistudio-credentials.ts +65 -0
- package/src/oauth/aistudio-native-daemon.ts +116 -0
- package/src/oauth/aistudio-session-sync.ts +95 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-aistudio-auth.ts +98 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +18 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/login-cli.ts +65 -1
- package/src/oauth/types.ts +15 -0
- package/src/providers/codex-capacity.ts +5 -2
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +29 -1
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +157 -46
- package/src/providers/registry.ts +184 -71
- package/src/providers/slug-codec.ts +52 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +34 -10
- package/src/responses/parser.ts +4 -0
- package/src/responses/schema.ts +5 -1
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/router.ts +43 -2
- package/src/routing/account-pool/cooldown.ts +8 -0
- package/src/routing/account-pool/index.ts +1 -0
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +24 -0
- package/src/server/chat-completions.ts +26 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/effort-policy.ts +16 -0
- package/src/server/index.ts +180 -14
- package/src/server/lifecycle.ts +52 -1
- package/src/server/management/agent-settings-routes.ts +31 -15
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +27 -9
- package/src/server/management/context.ts +9 -0
- package/src/server/management/logs-usage-routes.ts +11 -5
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/oauth-account-routes.ts +13 -3
- package/src/server/management/provider-routes.ts +137 -3
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/port-reclaim.ts +19 -1
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +33 -0
- package/src/server/request-log.ts +48 -21
- package/src/server/responses/collaboration.ts +42 -5
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +575 -140
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +14 -6
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +35 -5
- package/src/server/responses-custom-tool-repair.ts +52 -7
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/smoke/fingerprint-cache.ts +133 -0
- package/src/smoke/live-scenarios.ts +33 -0
- package/src/smoke/runner.ts +119 -0
- package/src/telemetry/dispatcher.ts +44 -0
- package/src/telemetry/fingerprint.ts +24 -0
- package/src/telemetry/hook.ts +43 -0
- package/src/telemetry/ledger.ts +54 -0
- package/src/telemetry/types.ts +23 -0
- package/src/types/config.ts +66 -14
- package/src/types/provider.ts +79 -1
- package/src/types/request.ts +18 -10
- package/src/types/tools.ts +30 -11
- package/src/types.ts +1 -0
- package/src/usage/command-code-manifest.ts +116 -0
- package/src/usage/cost.ts +2 -2
- package/src/usage/expected-prices.ts +126 -24
- package/src/usage/log.ts +18 -8
- package/src/usage/summary.ts +34 -12
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/index.ts +16 -8
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-DKLr4LTE.js +0 -102
- package/gui/dist/assets/index-DrSQdTRd.css +0 -1
|
@@ -7,6 +7,7 @@ import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, too
|
|
|
7
7
|
import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
|
|
8
8
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
9
9
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
10
|
+
import { debugDroppedFrame } from "../lib/debug";
|
|
10
11
|
import { configuredReasoningEfforts } from "../reasoning-effort";
|
|
11
12
|
import { commandCodeReasoningEfforts, refreshCommandCodeReasoningEfforts } from "../providers/command-code-efforts";
|
|
12
13
|
import { identifyRoutedModel } from "./identity";
|
|
@@ -33,9 +34,9 @@ export function formatCommandCodeErrorBody(_status: number, _headers: Headers, p
|
|
|
33
34
|
const payload = JSON.parse(payloadText) as unknown;
|
|
34
35
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return "";
|
|
35
36
|
const error = (payload as Record<string, unknown>).error;
|
|
36
|
-
const message = error && typeof error === "object" && !Array.isArray(error)
|
|
37
|
+
const message = (error && typeof error === "object" && !Array.isArray(error)
|
|
37
38
|
? (error as Record<string, unknown>).message
|
|
38
|
-
: undefined;
|
|
39
|
+
: undefined) ?? (payload as Record<string, unknown>).message;
|
|
39
40
|
return typeof message === "string" && message.trim()
|
|
40
41
|
? redactSecretString(message.trim()).slice(0, 400)
|
|
41
42
|
: "";
|
|
@@ -161,7 +162,8 @@ function wireMessages(messages: OcxMessage[]): Array<Record<string, unknown>> {
|
|
|
161
162
|
if (typeof message.content === "string") content.push({ type: "text", text: message.content });
|
|
162
163
|
else for (const part of message.content) {
|
|
163
164
|
if (part.type === "text") content.push({ type: "text", text: part.text });
|
|
164
|
-
else content.push(wireImagePart(part.imageUrl));
|
|
165
|
+
else if (part.type === "image") content.push(wireImagePart(part.imageUrl));
|
|
166
|
+
else content.push({ type: "text", text: "[video]" });
|
|
165
167
|
}
|
|
166
168
|
out.push({ role: "user", content });
|
|
167
169
|
}
|
|
@@ -174,13 +176,17 @@ function visibleTools(parsed: OcxParsedRequest): OcxTool[] {
|
|
|
174
176
|
if (choice === "none") return [];
|
|
175
177
|
const tools = parsed.context.tools ?? [];
|
|
176
178
|
if (isAllowedToolChoice(choice)) {
|
|
177
|
-
return tools
|
|
179
|
+
return tools
|
|
180
|
+
.filter(toolChoiceToolPredicate(choice, tools))
|
|
181
|
+
.sort((left, right) => namespacedToolName(left.namespace, left.name).localeCompare(namespacedToolName(right.namespace, right.name)));
|
|
178
182
|
}
|
|
179
183
|
if (choice && typeof choice !== "string") {
|
|
180
184
|
const selected = resolveToolChoiceWireName(tools, choice.name);
|
|
181
|
-
return tools
|
|
185
|
+
return tools
|
|
186
|
+
.filter(tool => namespacedToolName(tool.namespace, tool.name) === selected)
|
|
187
|
+
.sort((left, right) => namespacedToolName(left.namespace, left.name).localeCompare(namespacedToolName(right.namespace, right.name)));
|
|
182
188
|
}
|
|
183
|
-
return tools;
|
|
189
|
+
return [...tools].sort((left, right) => namespacedToolName(left.namespace, left.name).localeCompare(namespacedToolName(right.namespace, right.name)));
|
|
184
190
|
}
|
|
185
191
|
|
|
186
192
|
function toolChoiceInstruction(parsed: OcxParsedRequest): string | undefined {
|
|
@@ -211,6 +217,9 @@ function currentWorkingDirectory(): string | undefined {
|
|
|
211
217
|
|
|
212
218
|
/** Cap the workspace listing so a large directory does not ship every entry name upstream. */
|
|
213
219
|
const MAX_WORKSPACE_STRUCTURE_ENTRIES = 64;
|
|
220
|
+
/** Cap directory entries scanned while selecting the stable workspace prefix. */
|
|
221
|
+
export const MAX_WORKSPACE_STRUCTURE_SCAN_ENTRIES = 4096;
|
|
222
|
+
// ponytail: the bounded scan trades complete directory coverage for request latency; raise only with measured need.
|
|
214
223
|
/** Cap how many recent commit subjects the config carries. */
|
|
215
224
|
const MAX_RECENT_COMMITS = 8;
|
|
216
225
|
/** Cap each recent commit entry to keep the request bounded even for long subjects. */
|
|
@@ -227,22 +236,44 @@ function projectSlug(cwd: string): string {
|
|
|
227
236
|
return cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase().slice(0, 64) || "workspace";
|
|
228
237
|
}
|
|
229
238
|
|
|
239
|
+
function firstUserText(parsed: OcxParsedRequest): string | undefined {
|
|
240
|
+
for (const msg of parsed.context.messages) {
|
|
241
|
+
if (msg.role !== "user") continue;
|
|
242
|
+
if (typeof msg.content === "string") return msg.content;
|
|
243
|
+
const first = msg.content.find(part => part.type === "text" && typeof part.text === "string");
|
|
244
|
+
if (first && first.type === "text") return first.text;
|
|
245
|
+
}
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
230
249
|
export function commandCodeSessionId(parsed: OcxParsedRequest): string {
|
|
250
|
+
if (parsed._commandCodeSessionId) return parsed._commandCodeSessionId;
|
|
231
251
|
// Shared prompt-cache cohorts intentionally do not identify one conversation. Keep them out
|
|
232
252
|
// of upstream session affinity or unrelated conversations can pin to the same worker.
|
|
233
253
|
const threadId = parsed._clientThreadId?.trim();
|
|
234
254
|
const replayId = parsed._reasoningReplayScope?.clientThreadId?.trim();
|
|
255
|
+
const cursorId = parsed._cursorConversationId?.trim();
|
|
235
256
|
const cacheKey = !parsed._promptCacheKeyIsSharedCohort ? parsed.options.promptCacheKey?.trim() : undefined;
|
|
257
|
+
const rootText = firstUserText(parsed);
|
|
236
258
|
const identity = threadId
|
|
237
259
|
? ["thread", threadId]
|
|
238
260
|
: replayId
|
|
239
261
|
? ["replay", replayId]
|
|
240
|
-
:
|
|
241
|
-
? ["
|
|
242
|
-
:
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
262
|
+
: cursorId
|
|
263
|
+
? ["cursor", cursorId]
|
|
264
|
+
: cacheKey
|
|
265
|
+
? ["cache", cacheKey]
|
|
266
|
+
: rootText
|
|
267
|
+
? ["root", rootText]
|
|
268
|
+
: undefined;
|
|
269
|
+
const sessionId = !identity
|
|
270
|
+
? randomUUID()
|
|
271
|
+
: (() => {
|
|
272
|
+
const hex = createHash("sha256").update(`command-code:${identity[0]}\0${identity[1]}`).digest("hex");
|
|
273
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
274
|
+
})();
|
|
275
|
+
parsed._commandCodeSessionId = sessionId;
|
|
276
|
+
return sessionId;
|
|
246
277
|
}
|
|
247
278
|
|
|
248
279
|
interface GitWorkspaceInfo {
|
|
@@ -254,6 +285,8 @@ interface GitWorkspaceInfo {
|
|
|
254
285
|
}
|
|
255
286
|
|
|
256
287
|
export const workspaceMetadataCache = new Map<string, { collectedAt: number; value: GitWorkspaceInfo }>();
|
|
288
|
+
export const workspaceConfigCache = new Map<string, { collectedAt: number; value: Record<string, unknown>; sessionId?: string }>();
|
|
289
|
+
export const SESSION_WORKSPACE_CONFIG_TTL_MS = 60 * 60_000;
|
|
257
290
|
|
|
258
291
|
/**
|
|
259
292
|
* Evict expired entries first, then the oldest live entry if at capacity.
|
|
@@ -280,6 +313,34 @@ export function pruneWorkspaceMetadataCache(now: number): void {
|
|
|
280
313
|
}
|
|
281
314
|
}
|
|
282
315
|
|
|
316
|
+
export function pruneWorkspaceConfigCache(now: number): void {
|
|
317
|
+
for (const [key, entry] of workspaceConfigCache) {
|
|
318
|
+
if (!entry.sessionId && now - entry.collectedAt >= WORKSPACE_METADATA_TTL_MS) {
|
|
319
|
+
workspaceConfigCache.delete(key);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (workspaceConfigCache.size >= MAX_WORKSPACE_METADATA_ENTRIES) {
|
|
323
|
+
let oldestKey: string | null = null;
|
|
324
|
+
let oldestAt = Infinity;
|
|
325
|
+
for (const [key, entry] of workspaceConfigCache) {
|
|
326
|
+
if (entry.sessionId) continue;
|
|
327
|
+
if (entry.collectedAt < oldestAt) {
|
|
328
|
+
oldestAt = entry.collectedAt;
|
|
329
|
+
oldestKey = key;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (oldestKey === null) {
|
|
333
|
+
for (const [key, entry] of workspaceConfigCache) {
|
|
334
|
+
if (entry.collectedAt < oldestAt) {
|
|
335
|
+
oldestAt = entry.collectedAt;
|
|
336
|
+
oldestKey = key;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (oldestKey !== null) workspaceConfigCache.delete(oldestKey);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
283
344
|
const execFile = promisify(execFileCallback);
|
|
284
345
|
|
|
285
346
|
/** Best-effort git metadata for the upstream config contract; every read fails safe and stays off the event loop. */
|
|
@@ -316,32 +377,53 @@ async function gitWorkspaceInfo(cwd: string | undefined): Promise<GitWorkspaceIn
|
|
|
316
377
|
return value;
|
|
317
378
|
}
|
|
318
379
|
|
|
319
|
-
async function commandCodeConfig(cwd: string | undefined): Promise<Record<string, unknown>> {
|
|
380
|
+
export async function commandCodeConfig(cwd: string | undefined, sessionId?: string): Promise<Record<string, unknown>> {
|
|
381
|
+
const cacheKey = sessionId ? `${sessionId}:${cwd ?? ""}` : (cwd ?? "");
|
|
382
|
+
const now = Date.now();
|
|
383
|
+
const cached = cacheKey ? workspaceConfigCache.get(cacheKey) : undefined;
|
|
384
|
+
if (cacheKey) {
|
|
385
|
+
if (cached && (sessionId || now - cached.collectedAt < WORKSPACE_METADATA_TTL_MS)) return cached.value;
|
|
386
|
+
}
|
|
320
387
|
let structure: string[] = [];
|
|
321
388
|
if (cwd) {
|
|
322
389
|
try {
|
|
323
|
-
//
|
|
324
|
-
//
|
|
390
|
+
// Keep only the lexicographically smallest entries within the bounded scan so filesystem
|
|
391
|
+
// enumeration order cannot change the selected prefix for the scanned portion.
|
|
325
392
|
const dir = await opendir(cwd);
|
|
326
393
|
try {
|
|
327
|
-
|
|
394
|
+
const entries = dir[Symbol.asyncIterator]();
|
|
395
|
+
for (let scanned = 0; scanned < MAX_WORKSPACE_STRUCTURE_SCAN_ENTRIES; scanned += 1) {
|
|
396
|
+
const next = await entries.next();
|
|
397
|
+
if (next.done) break;
|
|
398
|
+
const entry = next.value;
|
|
328
399
|
if (entry.name.startsWith(".")) continue;
|
|
329
400
|
structure.push(entry.name);
|
|
330
|
-
if (structure.length
|
|
401
|
+
if (structure.length > MAX_WORKSPACE_STRUCTURE_ENTRIES) {
|
|
402
|
+
structure.sort();
|
|
403
|
+
structure.pop();
|
|
404
|
+
}
|
|
331
405
|
}
|
|
332
406
|
} finally {
|
|
333
407
|
await dir.close().catch(() => undefined);
|
|
334
408
|
}
|
|
335
409
|
} catch { /* workspace metadata is optional */ }
|
|
336
410
|
}
|
|
411
|
+
structure.sort();
|
|
337
412
|
const git = await gitWorkspaceInfo(cwd);
|
|
338
|
-
|
|
413
|
+
const value = {
|
|
339
414
|
...(cwd ? { workingDir: cwd } : {}),
|
|
340
|
-
date:
|
|
415
|
+
date: sessionId && typeof cached?.value.date === "string"
|
|
416
|
+
? cached.value.date
|
|
417
|
+
: new Date(now).toISOString().slice(0, 10),
|
|
341
418
|
environment: process.platform,
|
|
342
419
|
structure,
|
|
343
420
|
...git,
|
|
344
421
|
};
|
|
422
|
+
if (cacheKey) {
|
|
423
|
+
if (!workspaceConfigCache.has(cacheKey)) pruneWorkspaceConfigCache(now);
|
|
424
|
+
workspaceConfigCache.set(cacheKey, { collectedAt: now, value, ...(sessionId ? { sessionId } : {}) });
|
|
425
|
+
}
|
|
426
|
+
return value;
|
|
345
427
|
}
|
|
346
428
|
|
|
347
429
|
function usage(value: unknown): OcxUsage | undefined {
|
|
@@ -401,7 +483,7 @@ async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenera
|
|
|
401
483
|
let newline = buffer.indexOf("\n");
|
|
402
484
|
while (newline >= 0) {
|
|
403
485
|
const line = buffer.slice(0, newline).trim(); buffer = buffer.slice(newline + 1);
|
|
404
|
-
if (line)
|
|
486
|
+
if (line) yield* decodeEventLine(line);
|
|
405
487
|
newline = buffer.indexOf("\n");
|
|
406
488
|
}
|
|
407
489
|
const residualBytes = encoder.encode(buffer).byteLength;
|
|
@@ -412,7 +494,7 @@ async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenera
|
|
|
412
494
|
if (done) break;
|
|
413
495
|
}
|
|
414
496
|
const final = buffer.trim();
|
|
415
|
-
if (final)
|
|
497
|
+
if (final) yield* decodeEventLine(final);
|
|
416
498
|
} finally {
|
|
417
499
|
budget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
418
500
|
try { await reader.cancel(); } catch { /* already closed */ }
|
|
@@ -420,6 +502,41 @@ async function*ndjson(response: Response, budget: TranslatorBudget): AsyncGenera
|
|
|
420
502
|
}
|
|
421
503
|
}
|
|
422
504
|
|
|
505
|
+
/**
|
|
506
|
+
* Yield one NDJSON line as an event record, or nothing.
|
|
507
|
+
*
|
|
508
|
+
* `JSON.parse("null")` returns `null` instead of throwing, so the `try/catch` around the parse
|
|
509
|
+
* cannot see it and the `event.type` read in parseStream crashed the turn — the #1219 defect, on
|
|
510
|
+
* the one streaming transport the #1240 audit did not cover because it is NDJSON rather than SSE.
|
|
511
|
+
*
|
|
512
|
+
* A frame that does not parse to a record is padding, not an event: drop it and continue exactly
|
|
513
|
+
* as an unparseable line is already dropped, so a stream whose only frames are junk ends in the
|
|
514
|
+
* same single terminal `done` as an empty body. Skipping is what preserves an answer whose deltas
|
|
515
|
+
* have already arrived — the observed #1219 case is `null` padding BETWEEN content deltas, where
|
|
516
|
+
* terminating would discard a complete response (#1240).
|
|
517
|
+
*
|
|
518
|
+
* Note this deliberately makes a junk-only stream a quiet `[done]` where it previously threw. That
|
|
519
|
+
* throw was an unguarded type assumption, not a designed failure signal, and `[done]` is already
|
|
520
|
+
* what an empty body, a blank-line-only body and an unparseable-only body all produce here. The
|
|
521
|
+
* broader question — whether this adapter should report *any* no-valid-event stream as a failure
|
|
522
|
+
* rather than an empty success — is pre-existing, applies to all four of those inputs equally, and
|
|
523
|
+
* is deliberately not decided by this change.
|
|
524
|
+
*/
|
|
525
|
+
function* decodeEventLine(line: string): Generator<Record<string, unknown>> {
|
|
526
|
+
let parsed: unknown;
|
|
527
|
+
try {
|
|
528
|
+
parsed = JSON.parse(stripEventFrame(line));
|
|
529
|
+
} catch {
|
|
530
|
+
debugDroppedFrame("command-code", line);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
534
|
+
debugDroppedFrame("command-code", line);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
yield parsed as Record<string, unknown>;
|
|
538
|
+
}
|
|
539
|
+
|
|
423
540
|
/** The endpoint is newline-delimited JSON; defensively strip an SSE `data:` frame if the gateway ever switches shapes. */
|
|
424
541
|
function stripEventFrame(line: string): string {
|
|
425
542
|
return line.startsWith("data:") ? line.slice("data:".length).trim() : line;
|
|
@@ -490,6 +607,7 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
|
|
|
490
607
|
async buildRequest(parsed: OcxParsedRequest): Promise<AdapterRequest> {
|
|
491
608
|
if (!provider.apiKey) throw new Error("Command Code credential missing — run ocx login command-code");
|
|
492
609
|
const cwd = currentWorkingDirectory();
|
|
610
|
+
const sessionId = commandCodeSessionId(parsed);
|
|
493
611
|
const tools = visibleTools(parsed);
|
|
494
612
|
const toolNudge = buildNonOpenAIToolCatalogNudgeForTools(tools, parsed.options.toolChoice);
|
|
495
613
|
const choiceInstruction = toolChoiceInstruction(parsed);
|
|
@@ -500,7 +618,7 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
|
|
|
500
618
|
].join("\n\n"), parsed.modelId);
|
|
501
619
|
const reasoningEffort = supportedCommandCodeEffort(provider, parsed.modelId, parsed.options.reasoning);
|
|
502
620
|
const body = {
|
|
503
|
-
config: await commandCodeConfig(cwd), memory: "", taste: null, skills: null,
|
|
621
|
+
config: await commandCodeConfig(cwd, sessionId), memory: "", taste: null, skills: null,
|
|
504
622
|
permissionMode: "standard", mode: "agent",
|
|
505
623
|
params: {
|
|
506
624
|
model: canonicalCommandCodeModelId(parsed.modelId),
|
|
@@ -523,7 +641,7 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
|
|
|
523
641
|
"x-cli-environment": "production",
|
|
524
642
|
"x-taste-learning": "false",
|
|
525
643
|
"x-co-flag": "false",
|
|
526
|
-
"x-session-id":
|
|
644
|
+
"x-session-id": sessionId,
|
|
527
645
|
};
|
|
528
646
|
if (cwd) headers["x-project-slug"] = projectSlug(cwd);
|
|
529
647
|
return {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reversible single-line codec for Cursor composite tool-call ids.
|
|
3
|
+
*
|
|
4
|
+
* Cursor's wire delivers tool-call ids that can be two identifiers glued with a
|
|
5
|
+
* literal newline ("call-<uuid>-<n>\nfc_<uuid>_<n>"). OpenCodex forwards ids
|
|
6
|
+
* verbatim, so that newline leaked into Responses-visible `call_id` values,
|
|
7
|
+
* where line-oriented clients (logging, splitting, validation) break. The codec
|
|
8
|
+
* encodes only ids containing CR/LF into a versioned single-line form and
|
|
9
|
+
* decodes both that form and legacy raw multi-line ids back to the exact
|
|
10
|
+
* upstream bytes before anything is serialized toward Cursor.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const CALL_ID_PREFIX = "ocxc1_";
|
|
14
|
+
|
|
15
|
+
/** True when the id needs encoding to survive line-oriented consumers. */
|
|
16
|
+
function needsEncoding(id: string): boolean {
|
|
17
|
+
return id.includes("\n") || id.includes("\r");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Encode a Cursor wire call id into a single-line Responses-safe id. */
|
|
21
|
+
export function encodeCursorCallId(id: string): string {
|
|
22
|
+
if (!needsEncoding(id)) return id;
|
|
23
|
+
return CALL_ID_PREFIX + Buffer.from(id, "utf8").toString("base64url");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Decode a Responses-visible call id back to the exact Cursor wire id.
|
|
28
|
+
* Non-encoded ids (including legacy raw multi-line ids replayed by older
|
|
29
|
+
* clients) pass through unchanged; a malformed encoded payload also passes
|
|
30
|
+
* through rather than corrupting pairing.
|
|
31
|
+
*/
|
|
32
|
+
export function decodeCursorCallId(id: string): string {
|
|
33
|
+
if (!id.startsWith(CALL_ID_PREFIX)) return id;
|
|
34
|
+
const payload = id.slice(CALL_ID_PREFIX.length);
|
|
35
|
+
if (payload.length === 0) return id;
|
|
36
|
+
try {
|
|
37
|
+
const decoded = Buffer.from(payload, "base64url").toString("utf8");
|
|
38
|
+
// Round-trip guard: only trust payloads our encoder could have produced.
|
|
39
|
+
if (Buffer.from(decoded, "utf8").toString("base64url") !== payload) return id;
|
|
40
|
+
return decoded;
|
|
41
|
+
} catch {
|
|
42
|
+
return id;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -236,6 +236,7 @@ export function commitCursorCheckpoint(input: {
|
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
export function getCursorCheckpointForPrefix(input: {
|
|
239
|
+
conversationId: string;
|
|
239
240
|
prefixDigest: string;
|
|
240
241
|
systemDigest: string;
|
|
241
242
|
coveredMessageCount: number;
|
|
@@ -244,17 +245,21 @@ export function getCursorCheckpointForPrefix(input: {
|
|
|
244
245
|
}): CursorCheckpointSnapshot | undefined {
|
|
245
246
|
prune();
|
|
246
247
|
const refs = store.prefixIndex.get(input.prefixDigest);
|
|
247
|
-
if (!refs
|
|
248
|
-
const [ref] = refs;
|
|
249
|
-
if (!ref) return undefined;
|
|
250
|
-
const snapshot = getCursorCheckpoint(ref);
|
|
251
|
-
if (!snapshot) return undefined;
|
|
248
|
+
if (!refs) return undefined;
|
|
252
249
|
const identityScope = input.identityScope?.trim() || "local";
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
250
|
+
let foundRef: string | undefined;
|
|
251
|
+
for (const ref of refs) {
|
|
252
|
+
const snapshot = store.snapshots.get(ref);
|
|
253
|
+
if (!snapshot) continue;
|
|
254
|
+
if (snapshot.conversationId !== input.conversationId) continue;
|
|
255
|
+
if (snapshot.systemDigest !== input.systemDigest) continue;
|
|
256
|
+
if (snapshot.coveredMessageCount !== input.coveredMessageCount) continue;
|
|
257
|
+
if (snapshot.identityScope !== identityScope) continue;
|
|
258
|
+
if (snapshot.modelId !== input.modelId) continue;
|
|
259
|
+
if (foundRef) return undefined;
|
|
260
|
+
foundRef = ref;
|
|
261
|
+
}
|
|
262
|
+
return getCursorCheckpoint(foundRef);
|
|
258
263
|
}
|
|
259
264
|
|
|
260
265
|
export function getLatestCursorCheckpoint(
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CANONICAL_EFFORT_SUFFIXES,
|
|
3
3
|
cursorModelEffortLadder,
|
|
4
|
+
cursorModelHasEffortTiers,
|
|
4
5
|
cursorWireModelIdWithEffort,
|
|
6
|
+
CURSOR_THINKING_MODEL_IDS,
|
|
5
7
|
} from "./effort-map";
|
|
6
8
|
|
|
7
9
|
export interface CursorModelInfo {
|
|
@@ -152,6 +154,25 @@ export function cursorCodexToWireModelId(modelId: string): string {
|
|
|
152
154
|
return cursorWireModelSelection(modelId).modelId;
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Synthetic ultra/big-context picker marker (devlog 260826 070). A `cursor/<base>-1m` row is a
|
|
159
|
+
* picker-only variant: the wire request keeps `<base>` (plus effort suffix) and turns on Cursor
|
|
160
|
+
* Max Mode instead. Only ids listed here are treated as synthetic — a real upstream wire id that
|
|
161
|
+
* happens to end in `-1m` never collides because it will not be in this set.
|
|
162
|
+
*/
|
|
163
|
+
export const CURSOR_ULTRA_1M_MODEL_IDS: ReadonlySet<string> = new Set([
|
|
164
|
+
"kimi-k3-1m",
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
const CURSOR_ULTRA_1M_SUFFIX = "-1m";
|
|
168
|
+
|
|
169
|
+
/** Resolve a synthetic ultra marker id to its wire base, or undefined for ordinary ids. */
|
|
170
|
+
export function cursorUltraBaseModelId(modelId: string): string | undefined {
|
|
171
|
+
const normalized = modelId.startsWith("cursor/") ? modelId.slice("cursor/".length) : modelId;
|
|
172
|
+
if (!CURSOR_ULTRA_1M_MODEL_IDS.has(normalized)) return undefined;
|
|
173
|
+
return normalized.slice(0, -CURSOR_ULTRA_1M_SUFFIX.length);
|
|
174
|
+
}
|
|
175
|
+
|
|
155
176
|
/**
|
|
156
177
|
* Cursor-native wire models keep server-side conversation state reliably.
|
|
157
178
|
* External models (gpt/claude/gemini/grok families and similar) are more brittle on resumeAction.
|
|
@@ -212,10 +233,27 @@ export function filterCursorConfiguredModelsByLiveDiscovery<T extends { id: stri
|
|
|
212
233
|
liveIds: readonly string[],
|
|
213
234
|
): T[] {
|
|
214
235
|
return configured.filter(model =>
|
|
215
|
-
|
|
236
|
+
!CURSOR_KNOWN_UNCALLABLE_MODEL_IDS.has(model.id)
|
|
237
|
+
&& (
|
|
238
|
+
isCursorRouterModelId(model.id)
|
|
239
|
+
// Synthetic ultra rows ride their base model's account availability.
|
|
240
|
+
|| isCursorModelAvailableForAccount(cursorUltraBaseModelId(model.id) ?? model.id, liveIds)
|
|
241
|
+
),
|
|
216
242
|
);
|
|
217
243
|
}
|
|
218
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Models GetUsableModels advertises but whose every Run returns not_found (catalog honesty,
|
|
247
|
+
* devlog 260826_cursor_responses_gap 060). Live probes 2026-08-26: cursor/claude-opus-5 failed
|
|
248
|
+
* 100% ("Cursor Connect error not_found") while its -fast and -thinking siblings — separate
|
|
249
|
+
* wire families — succeed. Quarantined here, in the shared filter, so live, cached, stale, and
|
|
250
|
+
* static serving paths all agree. Custom user provider overrides are not routed through this
|
|
251
|
+
* canonical seed and stay untouched.
|
|
252
|
+
*/
|
|
253
|
+
export const CURSOR_KNOWN_UNCALLABLE_MODEL_IDS: ReadonlySet<string> = new Set([
|
|
254
|
+
"claude-opus-5",
|
|
255
|
+
]);
|
|
256
|
+
|
|
219
257
|
export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorModels([
|
|
220
258
|
// Context windows and the model lineup mirror Cursor's public models/pricing docs plus the jawcode
|
|
221
259
|
// SOT (../jawcode/packages/ai/src/models.json, `cursor` provider), which mirrors the real
|
|
@@ -243,7 +281,8 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
|
|
|
243
281
|
{ id: "claude-opus-4-7-fast", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
244
282
|
{ id: "claude-opus-4-8-fast", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
245
283
|
{ id: "claude-opus-4-8", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
246
|
-
|
|
284
|
+
// claude-opus-5 (bare) removed from the seed: GetUsableModels lists it but every Run returns
|
|
285
|
+
// not_found (quarantined via CURSOR_KNOWN_UNCALLABLE_MODEL_IDS; -fast/-thinking families stay).
|
|
247
286
|
{ id: "claude-opus-5-fast", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
248
287
|
{ id: "claude-fable-5", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
249
288
|
|
|
@@ -257,6 +296,21 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
|
|
|
257
296
|
{ id: "gemini-3-pro-image-preview", contextWindow: CONTEXT_200K },
|
|
258
297
|
{ id: "gemini-3.1-pro", contextWindow: CONTEXT_GEMINI },
|
|
259
298
|
{ id: "gemini-3.5-flash", contextWindow: CONTEXT_200K },
|
|
299
|
+
// 260825 live GetUsableModels: both ship only as effort-suffixed ids, so each exposes a tier
|
|
300
|
+
// picker. 3.6 is the only Cursor model with a `minimal` rung.
|
|
301
|
+
{ id: "gemini-3.6-flash", contextWindow: CONTEXT_GEMINI, supportsReasoningEffort: true },
|
|
302
|
+
{ id: "gemini-3.7-flash", contextWindow: CONTEXT_GEMINI, supportsReasoningEffort: true },
|
|
303
|
+
|
|
304
|
+
// Explicit-thinking variants (260825 live roster). Exposed as first-class ids the same way the
|
|
305
|
+
// Opus Fast families were in 831810c13: `isCursorModelAvailableForAccount` matches a base id
|
|
306
|
+
// against `{base}`, `{base}-{effort}` and the family's wire form, and none of those ever
|
|
307
|
+
// matched a `-thinking` id, so every one of these was invisible in the routed catalog.
|
|
308
|
+
// Suffix ORDER differs per family; `cursorWireModelIdWithEffort` owns that mapping.
|
|
309
|
+
...CURSOR_THINKING_MODEL_IDS.map(id => ({
|
|
310
|
+
id,
|
|
311
|
+
contextWindow: CONTEXT_200K,
|
|
312
|
+
supportsReasoningEffort: cursorModelHasEffortTiers(id),
|
|
313
|
+
})),
|
|
260
314
|
|
|
261
315
|
{ id: "gpt-5-codex", contextWindow: CONTEXT_272K },
|
|
262
316
|
{ id: "gpt-5-fast", contextWindow: CONTEXT_272K },
|
|
@@ -291,6 +345,10 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
|
|
|
291
345
|
// kimi-k3: cursor.com/docs/models/kimi-k3; account-verified via GetUsableModels (2026-07-28) —
|
|
292
346
|
// ships only as effort-suffixed kimi-k3-{low,high,max}, so the tier picker is exposed.
|
|
293
347
|
{ id: "kimi-k3", contextWindow: CONTEXT_262K, supportsReasoningEffort: true },
|
|
348
|
+
// kimi-k3-1m: synthetic ultra/Max-Mode picker variant (CURSOR_ULTRA_1M_MODEL_IDS) — wire sends
|
|
349
|
+
// kimi-k3-<effort> with maxMode=true; 1M context user-verified live on the Ultra plan
|
|
350
|
+
// (devlog 260826_cursor_responses_gap/025). inferCursorContextWindow maps "1m" ids to 1M.
|
|
351
|
+
{ id: "kimi-k3-1m", contextWindow: CONTEXT_1M, supportsReasoningEffort: true },
|
|
294
352
|
|
|
295
353
|
{ id: "grok-4.5", contextWindow: 500_000, supportsReasoningEffort: true },
|
|
296
354
|
{ id: "grok-4.5-fast", contextWindow: 500_000, supportsReasoningEffort: true },
|
|
@@ -34,12 +34,33 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
|
|
|
34
34
|
"claude-opus-5-fast": ["low", "medium", "high"],
|
|
35
35
|
"claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
|
|
36
36
|
"glm-5.2": ["high", "max"],
|
|
37
|
+
// 260825 live GetUsableModels. gemini-3.6-flash is the only Cursor model exposing `minimal`;
|
|
38
|
+
// listing it here is also what admits the suffix into CANONICAL_EFFORT_SUFFIXES below.
|
|
39
|
+
"gemini-3.6-flash": ["minimal", "low", "medium", "high"],
|
|
40
|
+
"gemini-3.7-flash": ["low", "medium", "high"],
|
|
41
|
+
// Explicit-thinking variants (260825 live roster). Tiers are the rungs the wire actually
|
|
42
|
+
// lists for each family, which is not always the same set the non-thinking id carries:
|
|
43
|
+
// 4.6-opus thinks only at high/max, 4.5-opus only at high, 4.6-sonnet only at medium.
|
|
44
|
+
"claude-opus-5-thinking": ["low", "medium", "high", "xhigh", "max"],
|
|
45
|
+
"claude-opus-5-thinking-fast": ["low", "medium", "high", "xhigh", "max"],
|
|
46
|
+
"claude-opus-4-8-thinking": ["low", "medium", "high", "xhigh", "max"],
|
|
47
|
+
"claude-opus-4-8-thinking-fast": ["low", "medium", "high", "xhigh", "max"],
|
|
48
|
+
"claude-opus-4-7-thinking": ["low", "medium", "high", "xhigh", "max"],
|
|
49
|
+
"claude-opus-4-7-thinking-fast": ["low", "medium", "high", "xhigh", "max"],
|
|
50
|
+
"claude-sonnet-5-thinking": ["low", "medium", "high", "xhigh", "max"],
|
|
51
|
+
"claude-fable-5-thinking": ["low", "medium", "high", "xhigh", "max"],
|
|
52
|
+
"claude-4.6-opus-thinking": ["high", "max"],
|
|
53
|
+
"claude-4.5-opus-thinking": ["high"],
|
|
54
|
+
"claude-4.6-sonnet-thinking": ["medium"],
|
|
37
55
|
// 260814 preemptive: glm-5.3 seeded ahead of Cursor's lineup update. Unlike 5.2, Z.AI folds
|
|
38
56
|
// 5.3 efforts into low/high/max (docs.z.ai/devpack/latest-model), so `low` is a real tier.
|
|
39
57
|
"glm-5.3": ["low", "high", "max"],
|
|
40
58
|
// GetUsableModels (2026-07-28) lists kimi-k3 only as effort-suffixed kimi-k3-{low,high,max};
|
|
41
59
|
// the bare id returns not_found. Tiers mirror the native Kimi provider's K3 ladder.
|
|
42
60
|
"kimi-k3": ["low", "high", "max"],
|
|
61
|
+
// Synthetic ultra picker variant (devlog 260826 070): same tier ladder as kimi-k3; the -1m
|
|
62
|
+
// marker is stripped before wire-id composition, so these tiers never form a wire suffix.
|
|
63
|
+
"kimi-k3-1m": ["low", "high", "max"],
|
|
43
64
|
// Cursor renamed the Grok 4.5 slugs to cursor-grok-4.5-{low,medium,high} and
|
|
44
65
|
// cursor-grok-4.5-{low,medium,high}-fast. The bare Fast id returns not_found.
|
|
45
66
|
"grok-4.5": ["low", "medium", "high"],
|
|
@@ -71,6 +92,46 @@ export const CANONICAL_EFFORT_SUFFIXES: ReadonlySet<string> = new Set([
|
|
|
71
92
|
|
|
72
93
|
const CANONICAL_CODEX_EFFORT_ORDER = ["low", "medium", "high", "xhigh", "max"] as const;
|
|
73
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Cursor's explicit-thinking variants, exposed as first-class Codex model ids the same way the
|
|
97
|
+
* `-fast` families were.
|
|
98
|
+
*
|
|
99
|
+
* `source` is the id whose wire name the variant is built from; `order` is where Cursor puts the
|
|
100
|
+
* thinking marker relative to the effort rung. All three shapes exist in the live roster
|
|
101
|
+
* (GetUsableModels, 260825), and using the wrong one is rejected with ERROR_BAD_MODEL_NAME:
|
|
102
|
+
*
|
|
103
|
+
* thinking-then-effort claude-opus-5-thinking-high, claude-opus-5-thinking-high-fast
|
|
104
|
+
* effort-then-thinking claude-4.6-opus-high-thinking
|
|
105
|
+
* bare claude-4.5-sonnet-thinking (the model has no effort rung)
|
|
106
|
+
*/
|
|
107
|
+
const CURSOR_THINKING_FAMILIES: Readonly<Record<string, { source: string; order: "thinking-then-effort" | "effort-then-thinking" | "bare" }>> = {
|
|
108
|
+
"claude-opus-5-thinking": { source: "claude-opus-5", order: "thinking-then-effort" },
|
|
109
|
+
"claude-opus-5-thinking-fast": { source: "claude-opus-5-fast", order: "thinking-then-effort" },
|
|
110
|
+
"claude-opus-4-8-thinking": { source: "claude-opus-4-8", order: "thinking-then-effort" },
|
|
111
|
+
"claude-opus-4-8-thinking-fast": { source: "claude-opus-4-8-fast", order: "thinking-then-effort" },
|
|
112
|
+
"claude-opus-4-7-thinking": { source: "claude-opus-4-7", order: "thinking-then-effort" },
|
|
113
|
+
"claude-opus-4-7-thinking-fast": { source: "claude-opus-4-7-fast", order: "thinking-then-effort" },
|
|
114
|
+
"claude-sonnet-5-thinking": { source: "claude-sonnet-5", order: "thinking-then-effort" },
|
|
115
|
+
"claude-fable-5-thinking": { source: "claude-fable-5", order: "thinking-then-effort" },
|
|
116
|
+
"claude-4.6-opus-thinking": { source: "claude-4.6-opus", order: "effort-then-thinking" },
|
|
117
|
+
"claude-4.5-opus-thinking": { source: "claude-4.5-opus", order: "effort-then-thinking" },
|
|
118
|
+
"claude-4.6-sonnet-thinking": { source: "claude-4.6-sonnet", order: "effort-then-thinking" },
|
|
119
|
+
"claude-4.5-sonnet-thinking": { source: "claude-4.5-sonnet", order: "bare" },
|
|
120
|
+
"claude-4-sonnet-thinking": { source: "claude-4-sonnet", order: "bare" },
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** Codex-facing ids for Cursor's explicit-thinking variants. */
|
|
124
|
+
export const CURSOR_THINKING_MODEL_IDS = Object.keys(CURSOR_THINKING_FAMILIES);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Picker order, which is the canonical ladder plus the declared sentinels that rank below `low`.
|
|
128
|
+
*
|
|
129
|
+
* `cursorModelEffortLadder` filters against this, so a tier absent from it is silently dropped
|
|
130
|
+
* from the Codex picker even though `cursorEffortSuffix` would happily send it. That is what
|
|
131
|
+
* hid `gemini-3.6-flash-minimal`, the one Cursor model with a `minimal` rung.
|
|
132
|
+
*/
|
|
133
|
+
const CURSOR_PICKER_EFFORT_ORDER = ["minimal", ...CANONICAL_CODEX_EFFORT_ORDER] as const;
|
|
134
|
+
|
|
74
135
|
function normalizeRequestedEffort(reasoning: string | undefined): string | undefined {
|
|
75
136
|
const normalized = reasoning?.toLowerCase();
|
|
76
137
|
return normalized === "ultra" ? "max" : normalized;
|
|
@@ -119,7 +180,7 @@ export function cursorModelEffortLadder(baseModelId: string): string[] | undefin
|
|
|
119
180
|
const tiers = CURSOR_MODEL_EFFORT_TIERS[baseModelId];
|
|
120
181
|
if (!tiers || tiers.length === 0) return undefined;
|
|
121
182
|
const tierSet = new Set(tiers);
|
|
122
|
-
return
|
|
183
|
+
return CURSOR_PICKER_EFFORT_ORDER.filter(effort => tierSet.has(effort));
|
|
123
184
|
}
|
|
124
185
|
|
|
125
186
|
/** Base models known to carry a reasoning-effort suffix (everything else is sent bare). */
|
|
@@ -133,6 +194,23 @@ export function cursorModelHasEffortTiers(baseModelId: string): boolean {
|
|
|
133
194
|
* and send the base model plus requested_model parameters instead.
|
|
134
195
|
*/
|
|
135
196
|
export function cursorWireModelIdWithEffort(baseModelId: string, effortSuffix: string): string {
|
|
197
|
+
const thinking = CURSOR_THINKING_FAMILIES[baseModelId];
|
|
198
|
+
if (thinking) {
|
|
199
|
+
const { source, order } = thinking;
|
|
200
|
+
// Cursor writes the thinking marker on either side of the effort depending on family
|
|
201
|
+
// (measured against GetUsableModels, 260825):
|
|
202
|
+
// thinking-then-effort claude-opus-5-thinking-high, ...-thinking-high-fast
|
|
203
|
+
// effort-then-thinking claude-4.6-opus-high-thinking
|
|
204
|
+
// bare claude-4.5-sonnet-thinking (no effort rung at all)
|
|
205
|
+
// Sending the wrong order returns ERROR_BAD_MODEL_NAME, so this is not cosmetic.
|
|
206
|
+
if (order === "bare") return `${source}-thinking`;
|
|
207
|
+
if (order === "effort-then-thinking") return `${source}-${effortSuffix}-thinking`;
|
|
208
|
+
if (source.endsWith("-fast")) {
|
|
209
|
+
const stem = source.slice(0, -"-fast".length);
|
|
210
|
+
return `${stem}-thinking-${effortSuffix}-fast`;
|
|
211
|
+
}
|
|
212
|
+
return `${source}-thinking-${effortSuffix}`;
|
|
213
|
+
}
|
|
136
214
|
if (baseModelId.endsWith("-fast")) {
|
|
137
215
|
return `${baseModelId.slice(0, -"-fast".length)}-${effortSuffix}-fast`;
|
|
138
216
|
}
|