ocx-cursor 0.2.0 → 0.3.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 +9 -0
- package/bin/ocx-cursor.mjs +7 -1
- package/package.json +1 -1
- package/src/catalog.mjs +34 -3
- package/src/cursor-state.mjs +54 -19
- package/src/gateway.mjs +18 -2
- package/src/install.mjs +12 -0
- package/src/paths.mjs +3 -0
package/README.md
CHANGED
|
@@ -185,6 +185,12 @@ The status output should show `Service: running` and `Gateway: healthy`. The pub
|
|
|
185
185
|
|
|
186
186
|
Open Cursor after both checks pass. Models with known reasoning controls show an effort value in the picker. Use `Shift+Command+/` to cycle it.
|
|
187
187
|
|
|
188
|
+
### Fast mode
|
|
189
|
+
|
|
190
|
+
OpenAI models whose OpenCodex catalog advertises the `priority` service tier show a Fast toggle in Cursor. Fast is off by default. When enabled, the bridge sends `service_tier: "priority"` to OpenCodex, which increases generation speed and consumes more usage.
|
|
191
|
+
|
|
192
|
+
Fast requires an OpenCodex version that preserves `service_tier` on its Chat Completions compatibility endpoint. Models without the `priority` tier, including Anthropic subscription models, do not receive the toggle.
|
|
193
|
+
|
|
188
194
|
## How requests are routed
|
|
189
195
|
|
|
190
196
|
```text
|
|
@@ -204,12 +210,15 @@ The gateway API key is generated during `init` and stored in Cursor with macOS S
|
|
|
204
210
|
| --- | --- |
|
|
205
211
|
| `ocx-cursor init [--base-url URL]` | Install the service, prompt for and test the tunnel, configure Cursor, and sync models. Cursor must be closed. |
|
|
206
212
|
| `ocx-cursor install` | Reinstall or restart the LaunchAgent without changing Cursor's API settings. |
|
|
213
|
+
| `ocx-cursor update` | Download the latest `ocx-cursor` release from npm, reinstall the service, and sync models. |
|
|
207
214
|
| `ocx-cursor sync` | Refresh the active model catalog. The service queues the update while Cursor runs. |
|
|
208
215
|
| `ocx-cursor status` | Show service health, model count, and pending sync state. |
|
|
209
216
|
| `ocx-cursor uninstall` | Remove the LaunchAgent, command link, and bridge home directory. |
|
|
210
217
|
|
|
211
218
|
`uninstall` leaves Cursor's custom endpoint and model records in its state database.
|
|
212
219
|
|
|
220
|
+
`update` preserves the existing gateway API key and Cursor endpoint. It updates only this companion package; update OpenCodex separately with your package manager.
|
|
221
|
+
|
|
213
222
|
## Model mapping
|
|
214
223
|
|
|
215
224
|
The bridge maps source model IDs to Cursor aliases:
|
package/bin/ocx-cursor.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import process from "node:process";
|
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { configureCursorOpenAI, storedCursorOpenAIBaseUrl } from "../src/cursor-config.mjs";
|
|
7
7
|
import { cursorIsRunning } from "../src/cursor-state.mjs";
|
|
8
|
-
import { installService, prepareInstallSecret, serviceStatus, uninstallService } from "../src/install.mjs";
|
|
8
|
+
import { installService, prepareInstallSecret, serviceStatus, uninstallService, updateService } from "../src/install.mjs";
|
|
9
9
|
import { cursorOpenAIBaseUrl, pendingFile } from "../src/paths.mjs";
|
|
10
10
|
import { runService } from "../src/service.mjs";
|
|
11
11
|
import { normalizeBaseUrl, testTunnel } from "../src/setup.mjs";
|
|
@@ -18,6 +18,7 @@ Usage:
|
|
|
18
18
|
Install the service, test the tunnel, configure Cursor,
|
|
19
19
|
and sync models
|
|
20
20
|
ocx-cursor install Install and start the macOS companion service
|
|
21
|
+
ocx-cursor update Install the latest companion release and restart it
|
|
21
22
|
ocx-cursor sync Sync active OpenCodex models into Cursor
|
|
22
23
|
ocx-cursor status Show service and model-sync status
|
|
23
24
|
ocx-cursor uninstall Stop and remove the companion service
|
|
@@ -109,6 +110,11 @@ async function main() {
|
|
|
109
110
|
printSync(await syncNow());
|
|
110
111
|
return;
|
|
111
112
|
}
|
|
113
|
+
if (command === "update") {
|
|
114
|
+
process.stdout.write("Updating OpenCodex Cursor Bridge from npm...\n");
|
|
115
|
+
updateService();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
112
118
|
if (command === "sync") {
|
|
113
119
|
printSync(await syncNow());
|
|
114
120
|
return;
|
package/package.json
CHANGED
package/src/catalog.mjs
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
codexConfigFile,
|
|
8
|
+
defaultCodexCatalogFile,
|
|
9
|
+
managedPrefix,
|
|
10
|
+
opencodexConfigFile,
|
|
11
|
+
opencodexServiceTokenFile,
|
|
12
|
+
} from "./paths.mjs";
|
|
6
13
|
|
|
7
14
|
export const allowedEfforts = ["low", "medium", "high", "xhigh", "max"];
|
|
8
15
|
export const effortLabels = {
|
|
@@ -44,7 +51,29 @@ export function sanitizeEfforts(sourceId, configured) {
|
|
|
44
51
|
return allowedEfforts.filter((effort) => values.includes(effort));
|
|
45
52
|
}
|
|
46
53
|
|
|
47
|
-
export function
|
|
54
|
+
export function configuredCodexCatalogFile(configFile = codexConfigFile) {
|
|
55
|
+
try {
|
|
56
|
+
const content = readFileSync(configFile, "utf8");
|
|
57
|
+
const match = content.match(/^\s*model_catalog_json\s*=\s*("(?:[^"\\]|\\.)*")\s*$/m);
|
|
58
|
+
if (match) return resolve(dirname(configFile), JSON.parse(match[1]));
|
|
59
|
+
} catch {}
|
|
60
|
+
return defaultCodexCatalogFile;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function priorityModelIds(catalogFile = configuredCodexCatalogFile()) {
|
|
64
|
+
try {
|
|
65
|
+
const payload = JSON.parse(readFileSync(catalogFile, "utf8"));
|
|
66
|
+
return new Set(payload.models
|
|
67
|
+
.filter((model) => Array.isArray(model?.service_tiers)
|
|
68
|
+
&& model.service_tiers.some((tier) => tier?.id === "priority"))
|
|
69
|
+
.map((model) => model.slug || model.id)
|
|
70
|
+
.filter((id) => typeof id === "string"));
|
|
71
|
+
} catch {
|
|
72
|
+
return new Set();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function normalizeActiveCatalog(configured, active, fastModelIds = new Set()) {
|
|
48
77
|
const configuredById = new Map(configured
|
|
49
78
|
.filter((model) => typeof model.provider === "string" && typeof model.model === "string")
|
|
50
79
|
.map((model) => [`${model.provider}/${model.model}`, model]));
|
|
@@ -69,6 +98,7 @@ export function normalizeActiveCatalog(configured, active) {
|
|
|
69
98
|
maxOutputTokens: model.capabilities?.max_output_tokens,
|
|
70
99
|
inputModalities,
|
|
71
100
|
reasoningEfforts: sanitizeEfforts(model.id, configuredModel?.reasoningEfforts ?? model.capabilities?.reasoning_effort),
|
|
101
|
+
supportsFast: fastModelIds.has(model.id),
|
|
72
102
|
});
|
|
73
103
|
}
|
|
74
104
|
|
|
@@ -113,5 +143,6 @@ export async function buildActiveCatalog(options = {}) {
|
|
|
113
143
|
return normalizeActiveCatalog(
|
|
114
144
|
configuredModels(options.ocxBin),
|
|
115
145
|
await activeModels(options.fetchImpl),
|
|
146
|
+
options.fastModelIds || priorityModelIds(options.codexCatalogFile),
|
|
116
147
|
);
|
|
117
148
|
}
|
package/src/cursor-state.mjs
CHANGED
|
@@ -42,26 +42,55 @@ function effortDefinition(model) {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function
|
|
45
|
+
function fastDefinition() {
|
|
46
|
+
return {
|
|
47
|
+
id: "fast",
|
|
48
|
+
name: "Fast",
|
|
49
|
+
markdownTooltip: "1.5x speed with increased usage.",
|
|
50
|
+
parameterType: {
|
|
51
|
+
booleanParameter: {
|
|
52
|
+
values: [
|
|
53
|
+
{ value: "false" },
|
|
54
|
+
{ value: "true", displayName: "Fast", increasesModelCost: true },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
isCycleableByHotkey: false,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function modelVariants(model) {
|
|
46
63
|
const parameterId = parameterIdFor(model);
|
|
47
64
|
const selectedDefault = defaultEffort(model);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
65
|
+
const efforts = model.reasoningEfforts.length > 0 ? model.reasoningEfforts : [null];
|
|
66
|
+
const fastValues = model.supportsFast ? ["false", "true"] : [null];
|
|
67
|
+
return efforts.flatMap((effort) => fastValues.map((fast) => {
|
|
68
|
+
const labels = [effort ? effortLabels[effort] : null, fast === "true" ? "Fast" : null].filter(Boolean);
|
|
69
|
+
const displayName = labels.length > 0
|
|
70
|
+
? `${model.alias} <span style="color: var(--cursor-text-tertiary);">${labels.join(" ")}</span>`
|
|
71
|
+
: model.alias;
|
|
72
|
+
const isDefault = (effort === null || effort === selectedDefault) && fast !== "true";
|
|
73
|
+
const parameters = [
|
|
74
|
+
...(effort ? [{ id: parameterId, value: effort }] : []),
|
|
75
|
+
...(fast ? [{ id: "fast", value: fast }] : []),
|
|
76
|
+
];
|
|
77
|
+
const suffix = [effort, fast === "true" ? "fast" : null].filter(Boolean).join("-");
|
|
51
78
|
return {
|
|
52
|
-
parameterValues:
|
|
79
|
+
parameterValues: parameters,
|
|
53
80
|
displayName,
|
|
54
81
|
displayNameOutsidePicker: displayName,
|
|
55
82
|
isMaxMode: false,
|
|
56
83
|
...(isDefault ? { isDefaultMaxConfig: true, isDefaultNonMaxConfig: true } : {}),
|
|
57
|
-
variantStringRepresentation: `${model.alias}[${
|
|
58
|
-
legacySlug: `${model.alias}-${
|
|
84
|
+
variantStringRepresentation: `${model.alias}[${parameters.map(({ id, value }) => `${id}=${value}`).join(",")}]`,
|
|
85
|
+
legacySlug: suffix ? `${model.alias}-${suffix}` : model.alias,
|
|
59
86
|
};
|
|
60
|
-
});
|
|
87
|
+
}));
|
|
61
88
|
}
|
|
62
89
|
|
|
63
90
|
export function cursorModel(model) {
|
|
64
91
|
const hasEffort = model.reasoningEfforts.length > 0;
|
|
92
|
+
const hasVariants = hasEffort || model.supportsFast;
|
|
93
|
+
const variants = hasVariants ? modelVariants(model) : [];
|
|
65
94
|
return {
|
|
66
95
|
name: model.alias,
|
|
67
96
|
defaultOn: false,
|
|
@@ -80,9 +109,12 @@ export function cursorModel(model) {
|
|
|
80
109
|
idAliases: [],
|
|
81
110
|
namedModelSectionIndex: 1,
|
|
82
111
|
cloudAgentEffortModes: [],
|
|
83
|
-
parameterDefinitions:
|
|
84
|
-
|
|
85
|
-
|
|
112
|
+
parameterDefinitions: [
|
|
113
|
+
...(hasEffort ? [effortDefinition(model)] : []),
|
|
114
|
+
...(model.supportsFast ? [fastDefinition()] : []),
|
|
115
|
+
],
|
|
116
|
+
variants,
|
|
117
|
+
legacySlugs: variants.map(({ legacySlug }) => legacySlug),
|
|
86
118
|
modelPickerBadges: [],
|
|
87
119
|
};
|
|
88
120
|
}
|
|
@@ -93,15 +125,18 @@ function syncSelectedModel(state, catalog) {
|
|
|
93
125
|
const byAlias = new Map(catalog.map((model) => [model.alias, model]));
|
|
94
126
|
for (const selected of composer.selectedModels) {
|
|
95
127
|
const model = byAlias.get(selected.modelId);
|
|
96
|
-
if (!model || model.reasoningEfforts.length === 0) continue;
|
|
128
|
+
if (!model || (model.reasoningEfforts.length === 0 && !model.supportsFast)) continue;
|
|
97
129
|
const parameterId = parameterIdFor(model);
|
|
98
|
-
const current = Array.isArray(selected.parameters)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
selected.parameters = [
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
130
|
+
const current = Array.isArray(selected.parameters) ? selected.parameters : [];
|
|
131
|
+
const effort = current.find(({ id }) => id === parameterId)?.value;
|
|
132
|
+
const fast = current.find(({ id }) => id === "fast")?.value;
|
|
133
|
+
selected.parameters = [
|
|
134
|
+
...(model.reasoningEfforts.length > 0 ? [{
|
|
135
|
+
id: parameterId,
|
|
136
|
+
value: model.reasoningEfforts.includes(effort) ? effort : defaultEffort(model),
|
|
137
|
+
}] : []),
|
|
138
|
+
...(model.supportsFast ? [{ id: "fast", value: fast === "true" ? "true" : "false" }] : []),
|
|
139
|
+
];
|
|
105
140
|
}
|
|
106
141
|
}
|
|
107
142
|
|
package/src/gateway.mjs
CHANGED
|
@@ -29,6 +29,13 @@ function suppliedEffort(payload, variantText) {
|
|
|
29
29
|
|| payload.reasoningEffort;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function suppliedFast(variantText) {
|
|
33
|
+
return variantText
|
|
34
|
+
?.split(",")
|
|
35
|
+
.map((value) => value.split("=", 2))
|
|
36
|
+
.find(([key]) => key === "fast")?.[1];
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
export function rewriteModelAliasBody(body, catalog) {
|
|
33
40
|
if (!body?.length) return body;
|
|
34
41
|
let payload;
|
|
@@ -42,10 +49,16 @@ export function rewriteModelAliasBody(body, catalog) {
|
|
|
42
49
|
const variant = /^(opencodex\/.+?)\[([^\]]+)\]$/.exec(payload.model);
|
|
43
50
|
let alias = variant?.[1] || payload.model;
|
|
44
51
|
let effort = suppliedEffort(payload, variant?.[2]);
|
|
52
|
+
let fast = suppliedFast(variant?.[2]);
|
|
45
53
|
if (!allowedEfforts.includes(effort)) {
|
|
46
|
-
const legacy = catalog.find((model) => model.reasoningEfforts?.some((value) =>
|
|
54
|
+
const legacy = catalog.find((model) => model.reasoningEfforts?.some((value) => (
|
|
55
|
+
payload.model === `${model.alias}-${value}` || payload.model === `${model.alias}-${value}-fast`
|
|
56
|
+
)));
|
|
47
57
|
if (legacy) {
|
|
48
|
-
effort = legacy.reasoningEfforts.find((value) =>
|
|
58
|
+
effort = legacy.reasoningEfforts.find((value) => (
|
|
59
|
+
payload.model === `${legacy.alias}-${value}` || payload.model === `${legacy.alias}-${value}-fast`
|
|
60
|
+
));
|
|
61
|
+
fast = payload.model.endsWith("-fast") ? "true" : "false";
|
|
49
62
|
alias = legacy.alias;
|
|
50
63
|
}
|
|
51
64
|
}
|
|
@@ -55,6 +68,8 @@ export function rewriteModelAliasBody(body, catalog) {
|
|
|
55
68
|
payload.model = catalogModel?.sourceId
|
|
56
69
|
|| (fallbackSourceId.startsWith("claude-") ? `anthropic/${fallbackSourceId}` : fallbackSourceId);
|
|
57
70
|
if (allowedEfforts.includes(effort)) payload.reasoning_effort = effort;
|
|
71
|
+
if (catalogModel?.supportsFast && fast === "true") payload.service_tier = "priority";
|
|
72
|
+
if (catalogModel?.supportsFast && fast === "false") delete payload.service_tier;
|
|
58
73
|
delete payload.reasoningEffort;
|
|
59
74
|
return Buffer.from(JSON.stringify(payload));
|
|
60
75
|
}
|
|
@@ -77,6 +92,7 @@ export function enrichModelList(active, catalog) {
|
|
|
77
92
|
supports_tool_use: true,
|
|
78
93
|
supports_streaming: true,
|
|
79
94
|
supports_reasoning: model.reasoningEfforts.length > 0,
|
|
95
|
+
supports_fast: model.supportsFast,
|
|
80
96
|
supports_vision: model.inputModalities.includes("image"),
|
|
81
97
|
reasoning_effort: model.reasoningEfforts,
|
|
82
98
|
},
|
package/src/install.mjs
CHANGED
|
@@ -176,6 +176,18 @@ export async function installService() {
|
|
|
176
176
|
return { installRoot, launchAgentFile, cliLinkFile, secretStatus };
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
export function updateService(options = {}) {
|
|
180
|
+
const execute = options.execFileSync || execFileSync;
|
|
181
|
+
execute(options.npmCommand || "npm", [
|
|
182
|
+
"exec",
|
|
183
|
+
"--yes",
|
|
184
|
+
"--package=ocx-cursor@latest",
|
|
185
|
+
"--",
|
|
186
|
+
"ocx-cursor",
|
|
187
|
+
"install",
|
|
188
|
+
], { stdio: "inherit" });
|
|
189
|
+
}
|
|
190
|
+
|
|
179
191
|
export async function uninstallService() {
|
|
180
192
|
bootout(serviceLabel);
|
|
181
193
|
await rm(launchAgentFile, { force: true });
|
package/src/paths.mjs
CHANGED
|
@@ -14,6 +14,9 @@ export const stderrFile = join(installRoot, "service.error.log");
|
|
|
14
14
|
export const launchAgentFile = join(homedir(), "Library", "LaunchAgents", `${serviceLabel}.plist`);
|
|
15
15
|
export const legacyLaunchAgentFile = join(homedir(), "Library", "LaunchAgents", `${legacyServiceLabel}.plist`);
|
|
16
16
|
export const cursorDatabaseFile = join(homedir(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
17
|
+
export const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
18
|
+
export const codexConfigFile = join(codexHome, "config.toml");
|
|
19
|
+
export const defaultCodexCatalogFile = join(codexHome, "opencodex-catalog.json");
|
|
17
20
|
export const opencodexConfigFile = join(homedir(), ".opencodex", "config.json");
|
|
18
21
|
export const opencodexServiceTokenFile = join(homedir(), ".opencodex", "service-api-token");
|
|
19
22
|
export const gatewayPort = Number(process.env.OCX_CURSOR_PORT || "10101");
|