ocx-cursor 1.0.6 → 1.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocx-cursor",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Use OpenCodex models in Cursor through an authenticated HTTPS gateway",
5
5
  "keywords": [
6
6
  "opencodex",
package/src/catalog.mjs CHANGED
@@ -39,7 +39,7 @@ export function aliasFor(sourceId) {
39
39
 
40
40
  export function inferredEfforts(sourceId) {
41
41
  const modelId = sourceId.split("/").at(-1);
42
- if (/^claude-(?:fable-5|sonnet-5|opus-(?:5|4-[78]))$/.test(modelId)) return [...allowedEfforts];
42
+ if (/^claude-(?:fable-5(?:-1)?|sonnet-5|opus-(?:5|4-[78]))$/.test(modelId)) return [...allowedEfforts];
43
43
  if (/^claude-(?:opus-4-6|sonnet-4-6)$/.test(modelId)) return ["low", "medium", "high", "max"];
44
44
  if (/^gpt-5\.6-(?:luna|sol|terra)$/.test(modelId)) return [...allowedEfforts];
45
45
  if (/^gpt-5\.[45](?:$|-)/.test(modelId)) return ["low", "medium", "high", "xhigh"];
@@ -73,38 +73,30 @@ export function priorityModelIds(catalogFile = configuredCodexCatalogFile()) {
73
73
  }
74
74
  }
75
75
 
76
- export function codexNativeModels(catalogFile = configuredCodexCatalogFile()) {
77
- try {
78
- const payload = JSON.parse(readFileSync(catalogFile, "utf8"));
79
- return payload.models
80
- .filter((model) => typeof model?.slug === "string"
81
- && !model.slug.includes("/")
82
- && model.visibility === "list"
83
- && model.supported_in_api === true
84
- && model.opencodex_capability_provenance === undefined)
85
- .map((model) => ({
86
- id: model.slug,
87
- owned_by: "openai",
88
- capabilities: {
89
- input_modalities: model.input_modalities,
90
- context_length: model.context_window,
91
- max_output_tokens: model.max_output_tokens,
92
- reasoning_effort: model.supported_reasoning_levels?.map(({ effort }) => effort),
93
- },
94
- }));
95
- } catch {
96
- return [];
97
- }
98
- }
99
-
100
76
  export function normalizeActiveCatalog(configured, active, fastModelIds = new Set()) {
101
77
  const configuredById = new Map(configured
102
78
  .filter((model) => typeof model.provider === "string" && typeof model.model === "string")
103
79
  .map((model) => [`${model.provider}/${model.model}`, model]));
104
80
  const models = new Map();
81
+ const activeIds = new Set(active.filter((model) => model?.owned_by !== "opencodex").map((model) => model?.id));
82
+ const configuredIds = new Set(configured.flatMap((model) => [
83
+ `${model.provider}/${model.model}`,
84
+ model.alias,
85
+ ...(model.provider === "openai" ? [model.model] : []),
86
+ ]));
87
+ // OpenCodex publishes synthetic --fast rows for clients without a Fast toggle.
88
+ // Preserve exact configured IDs and real product names ending in a single -fast.
89
+ const fastRows = new Set(active.filter((model) => (
90
+ typeof model?.id === "string"
91
+ && model.owned_by !== "opencodex"
92
+ && model.id.endsWith("--fast")
93
+ && !configuredIds.has(model.id)
94
+ && activeIds.has(model.id.slice(0, -"--fast".length))
95
+ )).map((model) => model.id));
105
96
 
106
97
  for (const model of active) {
107
98
  if (typeof model?.id !== "string" || model.owned_by === "opencodex") continue;
99
+ if (fastRows.has(model.id)) continue;
108
100
  const configuredModel = configuredById.get(model.id);
109
101
  const provider = model.id.includes("/") ? model.id.split("/", 1)[0] : String(model.owned_by || "openai").toLowerCase();
110
102
  if (provider === "cursor") continue;
@@ -123,7 +115,7 @@ export function normalizeActiveCatalog(configured, active, fastModelIds = new Se
123
115
  maxOutputTokens: model.capabilities?.max_output_tokens,
124
116
  inputModalities,
125
117
  reasoningEfforts: sanitizeEfforts(model.id, configuredModel?.reasoningEfforts ?? model.capabilities?.reasoning_effort),
126
- supportsFast: fastModelIds.has(model.id),
118
+ supportsFast: fastModelIds.has(model.id) || fastRows.has(`${model.id}--fast`),
127
119
  });
128
120
  }
129
121
 
@@ -168,7 +160,7 @@ export async function buildActiveCatalog(options = {}) {
168
160
  const codexCatalogFile = options.codexCatalogFile || configuredCodexCatalogFile();
169
161
  return normalizeActiveCatalog(
170
162
  configuredModels(options.ocxBin),
171
- [...await activeModels(options.fetchImpl), ...codexNativeModels(codexCatalogFile)],
163
+ await activeModels(options.fetchImpl),
172
164
  options.fastModelIds || priorityModelIds(codexCatalogFile),
173
165
  );
174
166
  }
@@ -21,7 +21,7 @@ export const legacyCursorSubagentCredentialsPatchMarker = "/*ocx-cursor-subagent
21
21
  export const cursorSubagentLegacyDetailsPatchMarker = "/*ocx-cursor-subagent-legacy-details-v2*/";
22
22
  export const legacyCursorSubagentLegacyDetailsPatchMarker = "/*ocx-cursor-subagent-legacy-details-v1*/";
23
23
  export const cursorSubagentPromptRoutingPatchMarker = "/*ocx-cursor-subagent-prompt-routing-v3*/";
24
- export const cursorSubagentExecutionRoutingPatchMarker = "/*ocx-cursor-subagent-execution-routing-v5*/";
24
+ export const cursorSubagentExecutionRoutingPatchMarker = "/*ocx-cursor-subagent-execution-routing-v6*/";
25
25
  export const cursorSubagentRunRoutingPatchMarker = "/*ocx-cursor-subagent-run-routing-v1*/";
26
26
  export const cursorSubagentRuntimeCredentialsPatchMarker = "/*ocx-cursor-subagent-runtime-credentials-v1*/";
27
27
  export const cursorSubagentModelDisplayPatchMarker = "/*ocx-cursor-subagent-model-display-v2*/";
@@ -60,6 +60,8 @@ const legacySubagentPromptRouting = /customSystemPrompt:\(\(\)=>\{\/\*ocx-cursor
60
60
  const legacySubagentRequestRouting = /(?<prefix>conversationState:[A-Za-z_$][\w$]*\([A-Za-z_$][\w$]*\),action:)\(\(\)=>\{\/\*ocx-cursor-subagent-prompt-routing-v2\*\/[\s\S]*?return (?<action>[A-Za-z_$][\w$]*)\}\)\(\)(?=,modelDetails:)/g;
61
61
  const createOrResumeSubagentStart = /async createOrResumeSubagent\((?<input>[A-Za-z_$][\w$]*)\)\{/g;
62
62
  const legacySubagentExecutionRouting = /(?<start>async createOrResumeSubagent\((?<input>[A-Za-z_$][\w$]*)\)\{)\/\*ocx-cursor-subagent-execution-routing-v[1-4]\*\/[\s\S]*?(?=let [A-Za-z_$][\w$]*,[A-Za-z_$][\w$]*=\k<input>\.resumeAgentId;)/g;
63
+ const currentSubagentExecutionRouting = /(?<start>async createOrResumeSubagent\((?<input>[A-Za-z_$][\w$]*)\)\{)\/\*ocx-cursor-subagent-execution-routing-v5\*\/[\s\S]*?(?=const [A-Za-z_$][\w$]*=\{mode:)/g;
64
+ const subagentSpawnMethod = /(?<start>async _createOrResumeSubagentSpawn\((?<input>[A-Za-z_$][\w$]*),[A-Za-z_$][\w$]*\)\{)(?<body>[\s\S]*?)(?=async runSubagentWithHandle\()/g;
63
65
  const legacySubagentModelConfigFixup = /(?<fixup>[A-Za-z_$][\w$]*)=ocxCursorRequestedModel\?void 0:typeof this\._modelConfigService\.fixupModelConfigForCurrentFlag=="function"\?this\._modelConfigService\.fixupModelConfigForCurrentFlag\(\{modelName:(?<input>[A-Za-z_$][\w$]*)\.modelId,maxMode:(?<composerConfig>[A-Za-z_$][\w$]*)\.maxMode===!0\}\):void 0/g;
64
66
  const subagentModelConfigFixup = /(?<fixup>[A-Za-z_$][\w$]*)=typeof this\._modelConfigService\.fixupModelConfigForCurrentFlag=="function"\?this\._modelConfigService\.fixupModelConfigForCurrentFlag\(\{modelName:(?<input>[A-Za-z_$][\w$]*)\.modelId,maxMode:(?<composerConfig>[A-Za-z_$][\w$]*)\.maxMode===!0\}\):void 0/g;
65
67
  const appendSubagentComposer = /(?<declaration>const (?<result>[A-Za-z_$][\w$]*)=await this\._composerDataService\.appendSubComposer\((?<composer>[A-Za-z_$][\w$]*)\))/g;
@@ -69,6 +71,7 @@ const requestedModelCredentials = /(?<prefix>new [A-Za-z_$][\w$]*\(\{modelId:(?<
69
71
  const subagentModelDisplaySelection = /function (?<functionName>[A-Za-z_$][\w$]*)\((?<input>[A-Za-z_$][\w$]*)\)\{return \k<input>\.resumeTargetComposerId!==void 0\?\k<input>\.subagentModelName:\k<input>\.bestOfNModelName!==void 0\?\k<input>\.bestOfNModelName:\k<input>\.taskParamModelName\?\?\k<input>\.subagentModelName\}/g;
70
72
  const legacySubagentModelDisplaySelection = /function (?<functionName>[A-Za-z_$][\w$]*)\((?<input>[A-Za-z_$][\w$]*)\)\{\/\*ocx-cursor-subagent-model-display-v1\*\/return \k<input>\.subagentModelName\?\.startsWith\("opencodex\/"\)\?\k<input>\.subagentModelName:\k<input>\.resumeTargetComposerId!==void 0\?\k<input>\.subagentModelName:\k<input>\.bestOfNModelName!==void 0\?\k<input>\.bestOfNModelName:\k<input>\.taskParamModelName\?\?\k<input>\.subagentModelName\}/g;
71
73
  const subagentTaskArgsContext = /(?<args>[A-Za-z_$][\w$]*)=(?<argsExtractor>[A-Za-z_$][\w$]*)\((?<bubble>[A-Za-z_$][\w$]*)\),(?<resume>[A-Za-z_$][\w$]*)=(?<resumeExtractor>[A-Za-z_$][\w$]*)\(\k<args>\),[\s\S]{0,400}?(?<taskModel>[A-Za-z_$][\w$]*)=\k<resume>===void 0\?\k<bubble>\.params\?\.model:void 0/g;
74
+ const directSubagentTaskArgsContext = /subagentTypeName:(?<bubble>[A-Za-z_$][\w$]*)\.params\?\.name[\s\S]{0,2500}?taskParamModelName:(?<taskModel>[A-Za-z_$][\w$]*)\}\);return/g;
72
75
  const legacySubagentTaskArgsDisplayV4 = /taskParamModelName:\/\*ocx-cursor-subagent-task-args-display-v4\*\/\(\(\)=>\{[\s\S]*?\}\)\(\)/g;
73
76
  const legacySubagentTaskArgsDisplayV3 = /taskParamModelName:\/\*ocx-cursor-subagent-task-args-display-v3\*\/\(\(\)=>\{[\s\S]*?return ocxCursorTaskModel\}\)\(\)/g;
74
77
  const legacySubagentTaskArgsDisplayV2 = /taskParamModelName:\/\*ocx-cursor-subagent-task-args-display-v2\*\/\(\(\)=>\{[\s\S]*?return ocxCursorTaskModel\}\)\(\)/g;
@@ -85,9 +88,29 @@ const directoryPatchQueues = new Map();
85
88
 
86
89
  export function clearCursorAppQuarantine(options = {}) {
87
90
  const execute = options.execFileSync || execFileSync;
88
- execute("/usr/bin/xattr", ["-dr", "com.apple.quarantine", options.appPath || cursorAppPath], {
89
- stdio: "ignore",
90
- });
91
+ const appPath = options.appPath || cursorAppPath;
92
+ try {
93
+ execute("/usr/bin/xattr", ["-dr", "com.apple.quarantine", appPath], {
94
+ stdio: "ignore",
95
+ });
96
+ } catch (error) {
97
+ // Read-only nested app directories can make recursive xattr traversal fail
98
+ // even after the bundle root has no quarantine attribute. List root
99
+ // attributes so probe failures and a real remaining quarantine are never
100
+ // hidden.
101
+ try {
102
+ const rootAttributes = execute("/usr/bin/xattr", [appPath], {
103
+ encoding: "utf8",
104
+ });
105
+ const hasRootQuarantine = String(rootAttributes)
106
+ .split(/\r?\n/)
107
+ .some((attribute) => attribute.trim() === "com.apple.quarantine");
108
+ if (!hasRootQuarantine) return;
109
+ } catch {
110
+ // Fail closed when the root probe itself cannot be completed.
111
+ }
112
+ throw error;
113
+ }
91
114
  }
92
115
 
93
116
  export function finalizeCursorAppPatch(options = {}) {
@@ -260,11 +283,34 @@ export function patchCursorSubagentPromptRoutingSource(source) {
260
283
  export function patchCursorSubagentExecutionRoutingSource(source) {
261
284
  if (source.includes(cursorSubagentExecutionRoutingPatchMarker)) return { status: "already-patched", source };
262
285
  let restored = source;
286
+ let restoredLegacySpawn = false;
287
+ if (restored.includes("/*ocx-cursor-subagent-execution-routing-v5*/")) {
288
+ restored = replaceSingleMatch(restored, currentSubagentExecutionRouting, (match) => match.groups.start, "current subagent execution routing");
289
+ const spawnMatches = [...restored.matchAll(subagentSpawnMethod)];
290
+ if (spawnMatches.length === 1) {
291
+ const { start, input, body } = spawnMatches[0].groups;
292
+ const routedBody = body
293
+ .replaceAll("ocxCursorRequestedModel?void 0:", `${input}.modelId?.startsWith("opencodex/")?void 0:`)
294
+ .replaceAll("ocxCursorRequestedModel&&", `${input}.modelId?.startsWith("opencodex/")&&`)
295
+ .replaceAll("modelName:ocxCursorRequestedModel", `modelName:${input}.modelId`)
296
+ .replaceAll("modelId:ocxCursorRequestedModel", `modelId:${input}.modelId`);
297
+ if (routedBody !== body) {
298
+ restored = replaceSingleMatch(restored, subagentSpawnMethod, `${start}${routedBody}`, "current subagent spawn routing");
299
+ restoredLegacySpawn = true;
300
+ }
301
+ }
302
+ }
263
303
  if (restored.includes("/*ocx-cursor-subagent-execution-routing-v4*/")) {
264
- restored = replaceSingleMatch(restored, legacySubagentModelConfigFixup, (match) => {
265
- const { fixup, input, composerConfig } = match.groups;
266
- return `${fixup}=typeof this._modelConfigService.fixupModelConfigForCurrentFlag=="function"?this._modelConfigService.fixupModelConfigForCurrentFlag({modelName:${input}.modelId,maxMode:${composerConfig}.maxMode===!0}):void 0`;
267
- }, "legacy subagent model config fixup");
304
+ const legacyFixups = [...restored.matchAll(legacySubagentModelConfigFixup)];
305
+ if (legacyFixups.length > 1) {
306
+ throw new Error(`Expected at most one Cursor legacy subagent model config fixup, found ${legacyFixups.length}`);
307
+ }
308
+ if (legacyFixups.length === 1) {
309
+ restored = replaceSingleMatch(restored, legacySubagentModelConfigFixup, (match) => {
310
+ const { fixup, input, composerConfig } = match.groups;
311
+ return `${fixup}=typeof this._modelConfigService.fixupModelConfigForCurrentFlag=="function"?this._modelConfigService.fixupModelConfigForCurrentFlag({modelName:${input}.modelId,maxMode:${composerConfig}.maxMode===!0}):void 0`;
312
+ }, "legacy subagent model config fixup");
313
+ }
268
314
  }
269
315
  if (restored.includes("/*ocx-cursor-subagent-execution-routing-v1*/") || restored.includes("/*ocx-cursor-subagent-execution-routing-v2*/") || restored.includes("/*ocx-cursor-subagent-execution-routing-v3*/") || restored.includes("/*ocx-cursor-subagent-execution-routing-v4*/")) {
270
316
  restored = replaceSingleMatch(restored, legacySubagentExecutionRouting, (match) => match.groups.start, "legacy subagent execution routing");
@@ -273,14 +319,28 @@ export function patchCursorSubagentExecutionRoutingSource(source) {
273
319
  const { input } = match.groups;
274
320
  return `${match[0]}${cursorSubagentExecutionRoutingPatchMarker}const ocxCursorModelMarker=typeof ${input}.prompt==="string"?/^\\s*<ocx-subagent-model>(opencodex\\/[A-Za-z0-9._\\/-]+)<\\/ocx-subagent-model>\\s*/.exec(${input}.prompt):null,ocxCursorRequestedModel=ocxCursorModelMarker?.[1];if(ocxCursorRequestedModel&&!${input}.resumeAgentId){const ocxCursorModelAvailable=this._modelConfigService.getAvailableDefaultModels().some(ocxCursorModel=>ocxCursorModel?.name===ocxCursorRequestedModel);if(!ocxCursorModelAvailable)throw new Error("OpenCodex subagent model is unavailable: "+ocxCursorRequestedModel);${input}={...${input},modelId:ocxCursorRequestedModel}}if(ocxCursorModelMarker)${input}={...${input},prompt:${input}.prompt.slice(ocxCursorModelMarker[0].length)};`;
275
321
  }, "subagent execution entrypoint");
276
- patched = replaceSingleMatch(patched, subagentModelConfigFixup, (match) => {
277
- const { fixup, input, composerConfig } = match.groups;
278
- return `${fixup}=ocxCursorRequestedModel?void 0:typeof this._modelConfigService.fixupModelConfigForCurrentFlag=="function"?this._modelConfigService.fixupModelConfigForCurrentFlag({modelName:${input}.modelId,maxMode:${composerConfig}.maxMode===!0}):void 0`;
279
- }, "subagent model config fixup");
280
- patched = replaceSingleMatch(patched, appendSubagentComposer, (match) => {
281
- const { declaration, composer } = match.groups;
282
- return `ocxCursorRequestedModel&&(${composer}.modelConfig={modelName:ocxCursorRequestedModel,maxMode:${composer}.modelConfig?.maxMode===!0,selectedModels:[{modelId:ocxCursorRequestedModel,parameters:[]}]});${declaration}`;
283
- }, "final subagent model config");
322
+ const configFixups = [...patched.matchAll(subagentModelConfigFixup)];
323
+ if (configFixups.length > 1) throw new Error(`Expected at most one Cursor subagent model config fixup, found ${configFixups.length}`);
324
+ const allowMissingConfigFixup = restoredLegacySpawn || source.includes("/*ocx-cursor-subagent-execution-routing-v4*/");
325
+ if (configFixups.length === 0 && !allowMissingConfigFixup) {
326
+ throw new Error("Expected one Cursor subagent model config fixup, found 0");
327
+ }
328
+ if (configFixups.length === 1) {
329
+ patched = replaceSingleMatch(patched, subagentModelConfigFixup, (match) => {
330
+ const { fixup, input, composerConfig } = match.groups;
331
+ // The execution entrypoint and the composer-spawn helper are separate
332
+ // methods in current Cursor builds. The requested model marker is scoped
333
+ // to createOrResumeSubagent, so derive routing from the already-normalized
334
+ // request model here instead of referencing that out-of-scope variable.
335
+ return `${fixup}=${input}.modelId?.startsWith("opencodex/")?void 0:typeof this._modelConfigService.fixupModelConfigForCurrentFlag=="function"?this._modelConfigService.fixupModelConfigForCurrentFlag({modelName:${input}.modelId,maxMode:${composerConfig}.maxMode===!0}):void 0`;
336
+ }, "subagent model config fixup");
337
+ }
338
+ if (!restoredLegacySpawn) {
339
+ patched = replaceSingleMatch(patched, appendSubagentComposer, (match) => {
340
+ const { declaration, composer } = match.groups;
341
+ return `(${composer}.modelConfig?.modelName?.startsWith("opencodex/"))&&(${composer}.modelConfig={modelName:${composer}.modelConfig.modelName,maxMode:${composer}.modelConfig?.maxMode===!0,selectedModels:[{modelId:${composer}.modelConfig.modelName,parameters:[]}]});${declaration}`;
342
+ }, "final subagent model config");
343
+ }
284
344
  return { status: "patched", source: patched };
285
345
  }
286
346
 
@@ -324,12 +384,17 @@ export function patchCursorSubagentModelDisplaySource(source) {
324
384
 
325
385
  export function patchCursorSubagentTaskArgsDisplaySource(source) {
326
386
  if (source.includes(cursorSubagentTaskArgsDisplayPatchMarker)) return { status: "already-patched", source };
327
- const contexts = [...source.matchAll(subagentTaskArgsContext)];
387
+ const legacyContexts = [...source.matchAll(subagentTaskArgsContext)];
388
+ const contexts = legacyContexts.length > 0
389
+ ? legacyContexts
390
+ : [...source.matchAll(directSubagentTaskArgsContext)];
328
391
  if (contexts.length !== 1) {
329
392
  throw new Error(`Expected one Cursor subagent task args context, found ${contexts.length}`);
330
393
  }
331
394
  const { args, bubble, taskModel } = contexts[0].groups;
332
- const replacement = `taskParamModelName:${cursorSubagentTaskArgsDisplayPatchMarker}/<ocx-subagent-model>(opencodex\\/[^<]+)<\\/ocx-subagent-model>/.exec(${bubble}.params?.prompt??${args}?.prompt??"")?.[1]??${taskModel}??${args}?.model`;
395
+ const prompt = args ? `${bubble}.params?.prompt??${args}?.prompt??""` : `${bubble}.params?.prompt??""`;
396
+ const fallback = args ? `${taskModel}??${args}?.model` : taskModel;
397
+ const replacement = `taskParamModelName:${cursorSubagentTaskArgsDisplayPatchMarker}/<ocx-subagent-model>(opencodex\\/[^<]+)<\\/ocx-subagent-model>/.exec(${prompt})?.[1]??${fallback}`;
333
398
  if (source.includes(legacyCursorSubagentTaskArgsDisplayV4PatchMarker)) {
334
399
  const upgraded = replaceSingleMatch(source, legacySubagentTaskArgsDisplayV4, replacement, "v4 subagent task args model display field");
335
400
  return { status: "patched", source: upgraded };
@@ -186,6 +186,17 @@ function syncSelectedModel(state, catalog) {
186
186
  const composer = state.aiSettings?.modelConfig?.composer;
187
187
  if (!composer || !Array.isArray(composer.selectedModels)) return;
188
188
  const byAlias = new Map(catalog.map((model) => [model.alias, model]));
189
+ for (const selected of composer.selectedModels) {
190
+ if (typeof selected?.modelId !== "string" || byAlias.has(selected.modelId)
191
+ || !selected.modelId.endsWith("--fast")) continue;
192
+ const base = byAlias.get(selected.modelId.slice(0, -"--fast".length));
193
+ if (!base?.supportsFast) continue;
194
+ selected.modelId = base.alias;
195
+ selected.parameters = [
196
+ ...(Array.isArray(selected.parameters) ? selected.parameters.filter(({ id }) => id !== "fast") : []),
197
+ { id: "fast", value: "true" },
198
+ ];
199
+ }
189
200
  composer.selectedModels = composer.selectedModels.filter((selected) => (
190
201
  typeof selected?.modelId !== "string" || !selected.modelId.startsWith(managedPrefix) || byAlias.has(selected.modelId)
191
202
  ));
package/src/gateway.mjs CHANGED
@@ -45,6 +45,122 @@ function disableStrictFunctionTools(tools) {
45
45
  }
46
46
  }
47
47
 
48
+ function normalizeCursorTool(tool) {
49
+ if (!isRecord(tool) || tool.type !== undefined || typeof tool.name !== "string" || !isRecord(tool.input_schema)) {
50
+ return tool;
51
+ }
52
+ const definition = {
53
+ name: tool.name,
54
+ ...(typeof tool.description === "string" ? { description: tool.description } : {}),
55
+ parameters: tool.input_schema,
56
+ ...(typeof tool.strict === "boolean" ? { strict: tool.strict } : {}),
57
+ };
58
+ return { type: "function", function: definition };
59
+ }
60
+
61
+ function normalizeToolChoice(choice) {
62
+ if (!isRecord(choice) || typeof choice.type !== "string") return choice;
63
+ if (choice.type === "auto") return "auto";
64
+ if (choice.type === "none") return "none";
65
+ if (choice.type === "any") return "required";
66
+ if (choice.type === "tool" && typeof choice.name === "string") {
67
+ return { type: "function", function: { name: choice.name } };
68
+ }
69
+ return choice;
70
+ }
71
+
72
+ function normalizeCursorTooling(payload) {
73
+ if (Array.isArray(payload.tools)) payload.tools = payload.tools.map(normalizeCursorTool);
74
+ if (payload.tool_choice !== undefined) payload.tool_choice = normalizeToolChoice(payload.tool_choice);
75
+ }
76
+
77
+ function toolResultText(value) {
78
+ if (typeof value === "string") return value;
79
+ if (Array.isArray(value)) return value.map(toolResultText).filter(Boolean).join("\n");
80
+ if (isRecord(value)) {
81
+ if (typeof value.text === "string") return value.text;
82
+ if (value.content !== undefined) return toolResultText(value.content);
83
+ try {
84
+ return JSON.stringify(value);
85
+ } catch {
86
+ return "";
87
+ }
88
+ }
89
+ return value === undefined || value === null ? "" : String(value);
90
+ }
91
+
92
+ function openAiToolCallFromCursorBlock(block) {
93
+ let args = "{}";
94
+ try {
95
+ args = JSON.stringify(block.input ?? {}) ?? "{}";
96
+ } catch {
97
+ // Keep the request valid if Cursor supplied a non-serializable input value.
98
+ }
99
+ return {
100
+ id: block.id,
101
+ type: "function",
102
+ function: {
103
+ name: block.name,
104
+ arguments: args,
105
+ },
106
+ };
107
+ }
108
+
109
+ export function normalizeCursorChatMessages(messages) {
110
+ if (!Array.isArray(messages)) return messages;
111
+ const normalized = [];
112
+ for (const message of messages) {
113
+ if (!isRecord(message) || !Array.isArray(message.content)) {
114
+ normalized.push(message);
115
+ continue;
116
+ }
117
+ if (message.role === "assistant") {
118
+ const allToolUses = message.content.filter((block) => isRecord(block) && block.type === "tool_use");
119
+ const toolUses = allToolUses.filter((block) => typeof block.id === "string" && block.id.length > 0 && typeof block.name === "string");
120
+ if (allToolUses.length > toolUses.length) {
121
+ normalized.push(message);
122
+ continue;
123
+ }
124
+ if (toolUses.length === 0) {
125
+ normalized.push(message);
126
+ continue;
127
+ }
128
+ const existingToolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
129
+ const existingIds = new Set(existingToolCalls.map((call) => call?.id).filter((id) => typeof id === "string"));
130
+ const newToolUses = toolUses.filter((block) => !existingIds.has(block.id));
131
+ const content = message.content.filter((block) => !(isRecord(block) && block.type === "tool_use"));
132
+ normalized.push({
133
+ ...message,
134
+ content: content.length > 0 ? content : null,
135
+ tool_calls: [
136
+ ...existingToolCalls,
137
+ ...newToolUses.map((block) => openAiToolCallFromCursorBlock(block)),
138
+ ],
139
+ });
140
+ continue;
141
+ }
142
+ const toolResults = message.content.filter((block) => isRecord(block) && block.type === "tool_result");
143
+ if (message.role !== "user" || toolResults.length === 0) {
144
+ normalized.push(message);
145
+ continue;
146
+ }
147
+ if (toolResults.some((block) => typeof block.tool_use_id !== "string" || block.tool_use_id.length === 0)) {
148
+ normalized.push(message);
149
+ continue;
150
+ }
151
+ for (const block of toolResults) {
152
+ normalized.push({
153
+ role: "tool",
154
+ tool_call_id: block.tool_use_id,
155
+ content: toolResultText(block.content),
156
+ });
157
+ }
158
+ const remainingContent = message.content.filter((block) => !(isRecord(block) && block.type === "tool_result"));
159
+ if (remainingContent.length > 0) normalized.push({ ...message, content: remainingContent });
160
+ }
161
+ return normalized;
162
+ }
163
+
48
164
  function enrichSubagentModelTools(tools, catalog) {
49
165
  if (!Array.isArray(tools)) return;
50
166
  const aliases = catalog.map(({ alias }) => alias).filter((alias) => alias.startsWith(managedPrefix));
@@ -91,7 +207,7 @@ function restoreRoutedSubagentModels(messages, catalog) {
91
207
  }
92
208
  }
93
209
 
94
- export function rewriteModelAliasBody(body, catalog) {
210
+ export function rewriteModelAliasBody(body, catalog, route = "POST /v1/chat/completions") {
95
211
  if (!body?.length) return body;
96
212
  let payload;
97
213
  try {
@@ -101,7 +217,11 @@ export function rewriteModelAliasBody(body, catalog) {
101
217
  }
102
218
  if (!isRecord(payload) || typeof payload.model !== "string" || !payload.model.startsWith(managedPrefix)) return body;
103
219
  restoreRoutedSubagentModels(payload.messages, catalog);
104
- enrichSubagentModelTools(payload.tools, catalog);
220
+ if (route === "POST /v1/chat/completions") {
221
+ payload.messages = normalizeCursorChatMessages(payload.messages);
222
+ normalizeCursorTooling(payload);
223
+ enrichSubagentModelTools(payload.tools, catalog);
224
+ }
105
225
 
106
226
  const variant = /^(opencodex\/.+?)\[([^\]]+)\]$/.exec(payload.model);
107
227
  let alias = variant?.[1] || payload.model;
@@ -251,7 +371,7 @@ export function startGateway(options) {
251
371
  const upstream = options.upstream || opencodexEndpoint();
252
372
  const receivedBody = await readBody(req);
253
373
  if (downstreamClosed) return;
254
- const body = rewriteModelAliasBody(receivedBody, catalog);
374
+ const body = rewriteModelAliasBody(receivedBody, catalog, route);
255
375
  const token = options.serviceToken !== undefined ? options.serviceToken : await serviceToken();
256
376
  if (downstreamClosed) return;
257
377
  upstreamReq = http.request({