ocx-cursor 0.5.0 → 0.5.2
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 +57 -195
- package/package.json +1 -1
- package/src/cursor-patch.mjs +112 -12
- package/src/cursor-state.mjs +16 -3
- package/src/gateway.mjs +29 -10
package/README.md
CHANGED
|
@@ -3,244 +3,116 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/ocx-cursor)
|
|
4
4
|
[](https://github.com/hiddenest/opencodex-cursor-bridge/actions/workflows/ci.yml)
|
|
5
5
|
|
|
6
|
-
Use active [OpenCodex](https://github.com/lidge-jun/opencodex) models in Cursor through
|
|
6
|
+
Use your active [OpenCodex](https://github.com/lidge-jun/opencodex) models in Cursor through a local OpenAI-compatible gateway.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- macOS with Cursor installed at `/Applications/Cursor.app`
|
|
11
|
-
- Node.js 22.5 or newer
|
|
12
|
-
- OpenCodex installed and configured
|
|
13
|
-
|
|
14
|
-
Launch Cursor and sign in once before setup. Cursor creates the Safe Storage key that the installer uses to encrypt the gateway API key.
|
|
15
|
-
|
|
16
|
-
## Setup
|
|
8
|
+
> [!WARNING]
|
|
9
|
+
> This package edits private JavaScript files inside `Cursor.app`. The changes invalidate Cursor's vendor signature, so Cursor may report that the installation is corrupt. The installer saves original files under `~/.opencodex/cursor-bridge/cursor-app-backups`. Reinstall Cursor to restore a signed app.
|
|
17
10
|
|
|
18
|
-
|
|
11
|
+
## Quick start
|
|
19
12
|
|
|
20
|
-
|
|
13
|
+
You need macOS, Node.js 22.5 or newer, and Cursor at `/Applications/Cursor.app`.
|
|
21
14
|
|
|
22
|
-
Install
|
|
15
|
+
Install and configure OpenCodex:
|
|
23
16
|
|
|
24
17
|
```bash
|
|
25
18
|
npm install --global @bitkyc08/opencodex
|
|
26
19
|
ocx init
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
The bridge service looks for `ocx` at `~/.local/bin/ocx` and `/opt/homebrew/bin/ocx`. If `command -v ocx` prints another path, link it into `~/.local/bin`:
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
mkdir -p ~/.local/bin
|
|
33
|
-
if [[ "$(command -v ocx)" != "$HOME/.local/bin/ocx" ]]; then
|
|
34
|
-
ln -sf "$(command -v ocx)" ~/.local/bin/ocx
|
|
35
|
-
fi
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
`ocx init` opens the interactive provider setup. You can also configure providers through the dashboard or terminal:
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
ocx gui
|
|
42
|
-
ocx login <provider>
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Install OpenCodex as a login service, then verify it:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
20
|
ocx service install
|
|
49
21
|
ocx service status
|
|
50
|
-
curl http://127.0.0.1:10100/v1/models
|
|
51
22
|
```
|
|
52
23
|
|
|
53
|
-
|
|
24
|
+
Check the configured models:
|
|
54
25
|
|
|
55
26
|
```bash
|
|
56
|
-
|
|
57
|
-
--header "Authorization: Bearer $(cat ~/.opencodex/service-api-token)" \
|
|
58
|
-
http://127.0.0.1:10100/v1/models
|
|
27
|
+
ocx models --json
|
|
59
28
|
```
|
|
60
29
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
### 2. Install the Cursor bridge
|
|
64
|
-
|
|
65
|
-
Quit Cursor completely, confirm that OpenCodex is running, then initialize the bridge:
|
|
30
|
+
Launch Cursor and sign in once, then quit it. Run the bridge installer:
|
|
66
31
|
|
|
67
32
|
```bash
|
|
68
33
|
npx ocx-cursor init
|
|
34
|
+
~/.local/bin/ocx-cursor status
|
|
69
35
|
```
|
|
70
36
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
1. Generates a gateway API key in `~/.opencodex/cursor-bridge/secret`.
|
|
74
|
-
2. Installs the `com.opencodex.cursor-bridge` LaunchAgent.
|
|
75
|
-
3. Tests the local gateway and OpenCodex model endpoint.
|
|
76
|
-
4. Stores the key in Cursor with macOS Safe Storage encryption.
|
|
77
|
-
5. Registers the local gateway as Cursor's OpenAI base URL.
|
|
78
|
-
6. Adds active OpenCodex models to Cursor under `opencodex/*`.
|
|
79
|
-
7. Enables Cursor's local-agent mode and installs the model metadata patches described below.
|
|
80
|
-
|
|
81
|
-
`init` patches ten JavaScript files inside `Cursor.app`:
|
|
82
|
-
|
|
83
|
-
- Eight Cursor bundles get the local-mode build flag.
|
|
84
|
-
- The Desktop and Glass workbench bundles retain OpenCodex display names, effort choices, and Fast metadata when Cursor refreshes its server catalog.
|
|
85
|
-
- Both local-agent runtimes display names such as `GPT 5.6 Sol` while keeping `opencodex/gpt-5.6-sol` as the request ID.
|
|
37
|
+
`init` registers `http://127.0.0.1:10101/v1` in Cursor. You do not need a domain or tunnel. Open Cursor after the status command reports `Service: running` and `Gateway: healthy`.
|
|
86
38
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
> [!WARNING]
|
|
90
|
-
> Patching `Cursor.app` invalidates its vendor code signature. Cursor may show “Your Cursor installation appears to be corrupt.” This is expected for this integration. Reinstall Cursor to restore an unmodified, vendor-signed app.
|
|
91
|
-
|
|
92
|
-
The installer links `ocx-cursor` into `~/.local/bin`. Add that directory to `PATH` if needed:
|
|
39
|
+
Add the installed command to your shell path if needed:
|
|
93
40
|
|
|
94
41
|
```bash
|
|
95
42
|
export PATH="$HOME/.local/bin:$PATH"
|
|
96
43
|
```
|
|
97
44
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
```bash
|
|
101
|
-
ocx-cursor status
|
|
102
|
-
curl http://127.0.0.1:10101/healthz
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
The status output should show `Service: running` and `Gateway: healthy`. Open Cursor after both checks pass.
|
|
106
|
-
|
|
107
|
-
To use an explicit HTTPS reverse proxy instead, pass its URL directly. This is optional and not used by the default setup:
|
|
108
|
-
|
|
109
|
-
```bash
|
|
110
|
-
npx ocx-cursor init --base-url https://cursor-api.example.com/v1
|
|
111
|
-
```
|
|
45
|
+
## What init changes
|
|
112
46
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
47
|
+
| Area | Change |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| Cursor settings | Stores the gateway key with macOS Safe Storage and sets the local base URL. |
|
|
50
|
+
| Cursor model state | Adds active models under `opencodex/*` with names, effort choices, and Fast metadata. |
|
|
51
|
+
| `Cursor.app` | Enables local-agent mode in eight bundles and patches two local-agent runtimes. |
|
|
52
|
+
| Login service | Installs `com.opencodex.cursor-bridge`, refreshes models, and reapplies patches after Cursor updates. |
|
|
118
53
|
|
|
119
|
-
|
|
54
|
+
Requests stay on loopback until OpenCodex sends them to your configured provider:
|
|
120
55
|
|
|
121
56
|
```text
|
|
122
|
-
Cursor
|
|
123
|
-
-> OpenCodex Cursor Bridge on http://127.0.0.1:10101/v1
|
|
124
|
-
-> OpenCodex on http://127.0.0.1:10100
|
|
125
|
-
-> configured provider
|
|
57
|
+
Cursor -> 127.0.0.1:10101 -> OpenCodex on 127.0.0.1:10100 -> provider
|
|
126
58
|
```
|
|
127
59
|
|
|
128
|
-
The gateway
|
|
129
|
-
|
|
130
|
-
## Commands
|
|
131
|
-
|
|
132
|
-
| Command | Purpose |
|
|
133
|
-
| --- | --- |
|
|
134
|
-
| `ocx-cursor init [--base-url URL]` | Install the service, test the endpoint, configure Cursor, and sync models. Cursor must be closed. |
|
|
135
|
-
| `ocx-cursor install` | Reinstall the LaunchAgent and verify app patches, or queue them until Cursor quits. |
|
|
136
|
-
| `ocx-cursor update` | Download the latest release from npm, reinstall the service, verify patches, and sync models. |
|
|
137
|
-
| `ocx-cursor sync` | Refresh the active model catalog. The service queues the update while Cursor runs. |
|
|
138
|
-
| `ocx-cursor launch` | Experimentally launch Cursor with live effort and Fast metadata injection. Keep the command running. |
|
|
139
|
-
| `ocx-cursor status` | Show service health, model count, and pending sync state. |
|
|
140
|
-
| `ocx-cursor uninstall` | Remove the LaunchAgent, command link, and bridge home directory. |
|
|
141
|
-
|
|
142
|
-
`uninstall` leaves Cursor's custom endpoint, model records, and app patches in place. Reinstall Cursor if you want to restore its original signed files.
|
|
60
|
+
The gateway requires its generated bearer token on every `/v1/*` request. It binds to `127.0.0.1` unless you change `OCX_CURSOR_HOST`.
|
|
143
61
|
|
|
144
|
-
|
|
62
|
+
Active OpenCodex `cursor/*` models are exposed under `opencodex/cursor/*`. The provider is experimental; the bridge cancels its upstream request when Cursor ends the matching client request.
|
|
145
63
|
|
|
146
|
-
## Model
|
|
64
|
+
## Model behavior
|
|
147
65
|
|
|
148
|
-
The bridge
|
|
66
|
+
The bridge reads OpenCodex's active `/v1/models` response and creates stable Cursor aliases:
|
|
149
67
|
|
|
150
68
|
```text
|
|
151
|
-
anthropic/claude-sonnet-5
|
|
152
|
-
gpt-5.6-sol
|
|
69
|
+
anthropic/claude-sonnet-5 -> opencodex/claude-sonnet-5
|
|
70
|
+
gpt-5.6-sol -> opencodex/gpt-5.6-sol
|
|
153
71
|
```
|
|
154
72
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
Cursor removes custom effort metadata from its database during startup. The companion LaunchAgent patches the Desktop and Glass catalog storage steps so `opencodex/*` display names, effort, and Fast metadata survive server refreshes. It monitors the installed bundles and local-agent runtimes, then reapplies their patches when Cursor replaces the app during an update.
|
|
158
|
-
|
|
159
|
-
`init`, `install`, and `update` also patch Cursor's local model runtimes so provider refreshes keep human-readable names such as `GPT 5.6 Sol` while requests continue using stable `opencodex/*` model IDs.
|
|
73
|
+
Cursor shows readable names such as `GPT 5.6 Sol`. Reasoning models receive an effort selector. Models with OpenCodex's `priority` service tier receive a Fast toggle.
|
|
160
74
|
|
|
161
|
-
Cursor
|
|
75
|
+
Cursor can send `strict: true` with function schemas that fail strict validation. The gateway changes the flag to `false` before forwarding those requests. It leaves tool names, descriptions, and arguments intact.
|
|
162
76
|
|
|
163
|
-
|
|
77
|
+
## Commands and configuration
|
|
164
78
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
ocx-cursor
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
## Configuration
|
|
79
|
+
| Command | Purpose |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `ocx-cursor init` | Install the service, configure Cursor, patch the app, and sync models. Cursor must be closed. |
|
|
82
|
+
| `ocx-cursor install` | Reinstall the service and check app patches. Running Cursor defers app changes until exit. |
|
|
83
|
+
| `ocx-cursor update` | Install the newest npm release and run `install`. |
|
|
84
|
+
| `ocx-cursor sync` | Refresh the active model list. |
|
|
85
|
+
| `ocx-cursor status` | Show service health and pending model sync. |
|
|
86
|
+
| `ocx-cursor uninstall` | Remove the service, command link, and bridge state. |
|
|
174
87
|
|
|
175
|
-
|
|
176
|
-
| --- | --- | --- |
|
|
177
|
-
| `OCX_CURSOR_BASE_URL` | `http://127.0.0.1:10101/v1` | Endpoint used by `init` when `--base-url` is absent. |
|
|
178
|
-
| `OCX_CURSOR_HOME` | `~/.opencodex/cursor-bridge` | Service state, API key, catalog, and logs. |
|
|
179
|
-
| `OCX_CURSOR_HOST` | `127.0.0.1` | Local gateway bind address. |
|
|
180
|
-
| `OCX_CURSOR_PORT` | `10101` | Local gateway port. Also changes the default Cursor endpoint. |
|
|
181
|
-
| `OCX_BIN` | `~/.local/bin/ocx` | OpenCodex CLI path. |
|
|
88
|
+
`uninstall` does not restore Cursor settings or patched app files. Reinstall Cursor if you want a clean vendor build.
|
|
182
89
|
|
|
183
|
-
|
|
90
|
+
| Variable | Default |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `OCX_CURSOR_BASE_URL` | `http://127.0.0.1:10101/v1` |
|
|
93
|
+
| `OCX_CURSOR_HOME` | `~/.opencodex/cursor-bridge` |
|
|
94
|
+
| `OCX_CURSOR_HOST` | `127.0.0.1` |
|
|
95
|
+
| `OCX_CURSOR_PORT` | `10101` |
|
|
96
|
+
| `OCX_BIN` | `~/.local/bin/ocx` |
|
|
184
97
|
|
|
185
|
-
|
|
186
|
-
- `POST /v1/chat/completions`
|
|
187
|
-
- `POST /v1/responses` and `/v1/responses/compact`
|
|
188
|
-
- `POST /v1/messages`
|
|
98
|
+
The bridge reads an OpenCodex service token from `~/.opencodex/service-api-token` when that file exists.
|
|
189
99
|
|
|
190
100
|
## Troubleshooting
|
|
191
101
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
### The local gateway is unavailable
|
|
201
|
-
|
|
202
|
-
Check the service and logs, then reinstall it if needed:
|
|
203
|
-
|
|
204
|
-
```bash
|
|
205
|
-
ocx-cursor status
|
|
206
|
-
tail -n 100 ~/.opencodex/cursor-bridge/service.error.log
|
|
207
|
-
tail -n 100 ~/.opencodex/cursor-bridge/service.log
|
|
208
|
-
ocx-cursor install
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### Models or effort options are missing
|
|
212
|
-
|
|
213
|
-
Quit Cursor and run:
|
|
214
|
-
|
|
215
|
-
```bash
|
|
216
|
-
ocx-cursor sync
|
|
217
|
-
ocx-cursor status
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
If the status shows a pending sync, wait until every Cursor Helper process has exited. The service applies the queued catalog automatically.
|
|
221
|
-
|
|
222
|
-
### Patches are missing after a Cursor update
|
|
223
|
-
|
|
224
|
-
Quit every Cursor window and wait for its Helper processes to stop. The LaunchAgent detects replaced app files and reapplies local mode, metadata, and display-name patches. Check the result with:
|
|
102
|
+
| Symptom | Fix |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| Cursor returns 401 | Quit Cursor and rerun `npx ocx-cursor init`. |
|
|
105
|
+
| Models or effort choices are missing | Quit Cursor, run `ocx-cursor sync`, then check `ocx-cursor status`. |
|
|
106
|
+
| A Cursor update removed the patches | Quit Cursor and run `ocx-cursor install`. |
|
|
107
|
+
| The gateway is unavailable | Run `ocx-cursor status` and inspect the logs below. |
|
|
108
|
+
| The bridge cannot find `ocx` | Run `ln -sf "$(command -v ocx)" ~/.local/bin/ocx`. |
|
|
225
109
|
|
|
226
110
|
```bash
|
|
227
|
-
ocx-cursor install
|
|
228
111
|
tail -n 100 ~/.opencodex/cursor-bridge/service.log
|
|
112
|
+
tail -n 100 ~/.opencodex/cursor-bridge/service.error.log
|
|
229
113
|
```
|
|
230
114
|
|
|
231
|
-
The patcher stops
|
|
232
|
-
|
|
233
|
-
### OpenCodex models cannot be loaded
|
|
234
|
-
|
|
235
|
-
Check OpenCodex first:
|
|
236
|
-
|
|
237
|
-
```bash
|
|
238
|
-
ocx status
|
|
239
|
-
ocx models --json
|
|
240
|
-
curl http://127.0.0.1:10100/v1/models
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
If `ocx models --json` works in your shell but fails in the bridge, check that `ocx` is available at `~/.local/bin/ocx` or `/opt/homebrew/bin/ocx`. The macOS LaunchAgent does not load your interactive shell profile.
|
|
115
|
+
The patcher stops when a Cursor update no longer matches its known code structure. It reports the file and mismatch in `service.error.log` without replacing that file.
|
|
244
116
|
|
|
245
117
|
## Development
|
|
246
118
|
|
|
@@ -250,14 +122,4 @@ npm run check
|
|
|
250
122
|
npm pack --dry-run
|
|
251
123
|
```
|
|
252
124
|
|
|
253
|
-
The
|
|
254
|
-
|
|
255
|
-
## Compatibility
|
|
256
|
-
|
|
257
|
-
The package writes Cursor's local state database, uses Cursor's Safe Storage format, and modifies private code inside `Cursor.app`. Cursor does not document these interfaces. A Cursor update can change them.
|
|
258
|
-
|
|
259
|
-
The current release supports macOS. It does not configure Windows Credential Manager, Linux keyrings, or system services outside launchd.
|
|
260
|
-
|
|
261
|
-
## License
|
|
262
|
-
|
|
263
|
-
MIT
|
|
125
|
+
The project uses the MIT license.
|
package/package.json
CHANGED
package/src/cursor-patch.mjs
CHANGED
|
@@ -9,28 +9,44 @@ import {
|
|
|
9
9
|
cursorWorkbenchFile,
|
|
10
10
|
} from "./paths.mjs";
|
|
11
11
|
|
|
12
|
-
export const cursorPatchMarker = "/*ocx-cursor-model-metadata-
|
|
12
|
+
export const cursorPatchMarker = "/*ocx-cursor-model-metadata-v5*/";
|
|
13
13
|
export const legacyCursorPatchMarker = "/*ocx-cursor-model-metadata*/";
|
|
14
14
|
export const cursorLocalModeDisabled = "localMode:!1";
|
|
15
15
|
export const cursorLocalModeEnabled = "localMode:!0";
|
|
16
|
-
export const cursorLocalRuntimePatchMarker = "/*ocx-cursor-local-model-display*/";
|
|
16
|
+
export const cursorLocalRuntimePatchMarker = "/*ocx-cursor-local-model-display-v2*/";
|
|
17
|
+
export const legacyCursorLocalRuntimePatchMarker = "/*ocx-cursor-local-model-display*/";
|
|
18
|
+
export const cursorLocalRuntimeCapabilitiesPatchMarker = "/*ocx-cursor-local-model-capabilities-v3*/";
|
|
19
|
+
export const legacyCursorLocalRuntimeCapabilitiesPatchMarker = "/*ocx-cursor-local-model-capabilities-v2*/";
|
|
17
20
|
|
|
18
21
|
export function isCursorModelMetadataBundle(file) {
|
|
19
22
|
return file === cursorWorkbenchFile || file === cursorGlassWorkbenchFile;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
const catalogNormalization = /(?<normalization>\b(?<catalog>[A-Za-z_$][\w$]*)=\k<catalog>\.map\((?<item>[A-Za-z_$][\w$]*)=>(?<plain>[A-Za-z_$][\w$]*)\(\k<item>\)\)),(?=(?<batch>[A-Za-z_$][\w$]*)\(\(\)=>\{this\._reactiveStorageService\.setApplicationUserPersistentStorage\("availableDefaultModels2",\k<catalog>\))/g;
|
|
23
|
-
const previousCatalogInjection = /\/\*ocx-cursor-model-metadata(?:-
|
|
26
|
+
const previousCatalogInjection = /\/\*ocx-cursor-model-metadata(?:-v[234])?\*\/(?<catalog>[A-Za-z_$][\w$]*)=\k<catalog>\.map\(ocxCursorModel=>\{.*?\}\),(?=(?<batch>[A-Za-z_$][\w$]*)\(\(\)=>\{this\._reactiveStorageService\.setApplicationUserPersistentStorage\("availableDefaultModels2",\k<catalog>\))/g;
|
|
24
27
|
const localModelConstructor = /function (?<functionName>[A-Za-z_$][\w$]*)\(e,t\)\{return new (?<modelType>[A-Za-z_$][\w$]*\.[A-Za-z_$][\w$]*)\(\{modelId:e,displayModelId:e,displayName:null!=t\?t:e,displayNameShort:null!=t\?t:e,aliases:\[\]\}\)\}/g;
|
|
28
|
+
const localRuntimeVisionCapability = /"boolean"==typeof (?<object>[A-Za-z_$][\w$]*)\.supports_vision\?\{supports_vision:\k<object>\.supports_vision\}:\{\}\)/g;
|
|
29
|
+
const localRuntimePickerInput = /toPickerInput\((?<model>[A-Za-z_$][\w$]*)\)\{var [A-Za-z_$][\w$]*;const [A-Za-z_$][\w$]*=this\.modelMetadataById\.get\(\k<model>\.modelId\),(?<capabilities>[A-Za-z_$][\w$]*)=.*?;return\{.*?supportsReasoning:[^,]+,supportsVision:/g;
|
|
30
|
+
const localRuntimeProviderPickerInput = /supportsReasoning:(?<reasoning>!0===\(null===\((?<capability>[A-Za-z_$][\w$]*)=(?<model>[A-Za-z_$][\w$]*)\.capabilities\)\|\|void 0===\k<capability>\?void 0:\k<capability>\.supports_reasoning\)),supportsVision:/g;
|
|
31
|
+
const localRuntimeRequestParameters = /function\((?<payload>[A-Za-z_$][\w$]*),(?<model>[A-Za-z_$][\w$]*),(?<parameters>[A-Za-z_$][\w$]*),(?<apiType>[A-Za-z_$][\w$]*),(?<route>[A-Za-z_$][\w$]*),(?<extended>[A-Za-z_$][\w$]*)=!1\)\{(?=const [A-Za-z_$][\w$]*=function\([A-Za-z_$][\w$]*\)\{var [A-Za-z_$][\w$]*;const [A-Za-z_$][\w$]*=.*?\["reasoning","effort","thought_level"\]\.includes)/g;
|
|
32
|
+
const localRuntimeBuilderExport = /buildBottlerocketPickerModels:\(\)=>(?<builder>[A-Za-z_$][\w$]*)/g;
|
|
25
33
|
const transientPatchErrorCodes = new Set(["EACCES", "EBUSY", "EPERM"]);
|
|
26
34
|
|
|
35
|
+
function replaceSingleMatch(source, pattern, replacement, label) {
|
|
36
|
+
const matches = [...source.matchAll(pattern)];
|
|
37
|
+
if (matches.length !== 1) throw new Error(`Expected one Cursor ${label}, found ${matches.length}`);
|
|
38
|
+
const match = matches[0];
|
|
39
|
+
const value = typeof replacement === "function" ? replacement(match) : replacement;
|
|
40
|
+
return `${source.slice(0, match.index)}${value}${source.slice(match.index + match[0].length)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
function metadataInjection(catalog) {
|
|
28
|
-
return `${cursorPatchMarker}${catalog}=${catalog}.map(ocxCursorModel=>{if(!ocxCursorModel.name.startsWith("opencodex/"))return ocxCursorModel;const ocxCursorStored=(this._reactiveStorageService.applicationUserPersistentStorage.availableDefaultModels2??[]).find(ocxCursorCandidate=>ocxCursorCandidate?.name===ocxCursorModel.name);const ocxCursorDisplayWords={claude:"Claude",codex:"Codex",composer:"Composer",deepseek:"DeepSeek",fable:"Fable",fast:"Fast",flash:"Flash",gpt:"GPT",grok:"Grok",hy3:"HY3",kimi:"Kimi",luna:"Luna",max:"Max",mimo:"MiMo",mini:"Mini",opus:"Opus",pro:"Pro",qwen:"Qwen",sol:"Sol",sonnet:"Sonnet",spark:"Spark",terra:"Terra"};const ocxCursorDisplayName=ocxCursorModel.name.split("/").at(-1).split("-").map(ocxCursorWord=>{if(ocxCursorDisplayWords[ocxCursorWord])return ocxCursorDisplayWords[ocxCursorWord];const ocxCursorAttached=/^(qwen|kimi|gpt|claude|grok)(\\d+(?:\\.\\d+)*)$/.exec(ocxCursorWord);if(ocxCursorAttached)return ocxCursorDisplayWords[ocxCursorAttached[1]]+" "+ocxCursorAttached[2];if(/^v\\d/i.test(ocxCursorWord))return"V"+ocxCursorWord.slice(1);if(/^k\\d/i.test(ocxCursorWord))return"K"+ocxCursorWord.slice(1);if(/^\\d/.test(ocxCursorWord))return ocxCursorWord;return ocxCursorWord.slice(0,1).toUpperCase()+ocxCursorWord.slice(1)}).join(" ");const ocxCursorPrettyLabel=ocxCursorValue=>typeof ocxCursorValue==="string"?ocxCursorValue.split(ocxCursorModel.name).join(ocxCursorDisplayName):ocxCursorValue;const
|
|
44
|
+
return `${cursorPatchMarker}${catalog}=${catalog}.map(ocxCursorModel=>{if(!ocxCursorModel.name.startsWith("opencodex/"))return ocxCursorModel;const ocxCursorStored=(this._reactiveStorageService.applicationUserPersistentStorage.availableDefaultModels2??[]).find(ocxCursorCandidate=>ocxCursorCandidate?.name===ocxCursorModel.name);const ocxCursorDisplayWords={claude:"Claude",codex:"Codex",composer:"Composer",deepseek:"DeepSeek",fable:"Fable",fast:"Fast",flash:"Flash",gpt:"GPT",grok:"Grok",hy3:"HY3",kimi:"Kimi",luna:"Luna",max:"Max",mimo:"MiMo",mini:"Mini",opus:"Opus",pro:"Pro",qwen:"Qwen",sol:"Sol",sonnet:"Sonnet",spark:"Spark",terra:"Terra"};const ocxCursorDisplayName=ocxCursorStored?.clientDisplayName??((ocxCursorModel.name.startsWith("opencodex/cursor/")?"Cursor ":"")+ocxCursorModel.name.split("/").at(-1).split("-").map(ocxCursorWord=>{if(ocxCursorDisplayWords[ocxCursorWord])return ocxCursorDisplayWords[ocxCursorWord];const ocxCursorAttached=/^(qwen|kimi|gpt|claude|grok)(\\d+(?:\\.\\d+)*)$/.exec(ocxCursorWord);if(ocxCursorAttached)return ocxCursorDisplayWords[ocxCursorAttached[1]]+" "+ocxCursorAttached[2];if(/^v\\d/i.test(ocxCursorWord))return"V"+ocxCursorWord.slice(1);if(/^k\\d/i.test(ocxCursorWord))return"K"+ocxCursorWord.slice(1);if(/^\\d/.test(ocxCursorWord))return ocxCursorWord;return ocxCursorWord.slice(0,1).toUpperCase()+ocxCursorWord.slice(1)}).join(" "));const ocxCursorPrettyLabel=ocxCursorValue=>typeof ocxCursorValue==="string"?ocxCursorValue.split(ocxCursorModel.name).join(ocxCursorDisplayName):ocxCursorValue;const ocxCursorDefinitions=Array.isArray(ocxCursorModel.parameterDefinitions)&&ocxCursorModel.parameterDefinitions.length>0?ocxCursorModel.parameterDefinitions:ocxCursorStored?.parameterDefinitions??[];const ocxCursorVariantSource=Array.isArray(ocxCursorModel.variants)&&ocxCursorModel.variants.length>0?ocxCursorModel.variants:ocxCursorStored?.variants??[];const ocxCursorVariants=ocxCursorVariantSource.map(ocxCursorVariant=>({...ocxCursorVariant,displayName:ocxCursorPrettyLabel(ocxCursorVariant.displayName),displayNameOutsidePicker:ocxCursorPrettyLabel(ocxCursorVariant.displayNameOutsidePicker)}));const ocxCursorLegacySlugs=Array.isArray(ocxCursorModel.legacySlugs)&&ocxCursorModel.legacySlugs.length>0?ocxCursorModel.legacySlugs:ocxCursorStored?.legacySlugs??[];return{...ocxCursorModel,clientDisplayName:ocxCursorDisplayName,inputboxShortModelName:ocxCursorDisplayName,parameterDefinitions:ocxCursorDefinitions,variants:ocxCursorVariants,legacySlugs:ocxCursorLegacySlugs,supportsThinking:void 0!==ocxCursorModel.supportsThinking?ocxCursorModel.supportsThinking:ocxCursorStored?.supportsThinking}}),`;
|
|
29
45
|
}
|
|
30
46
|
|
|
31
47
|
export function patchCursorWorkbenchSource(source) {
|
|
32
48
|
if (source.includes(cursorPatchMarker)) return { status: "already-patched", source };
|
|
33
|
-
if (source.includes(legacyCursorPatchMarker) || source.includes("/*ocx-cursor-model-metadata-v2*/")) {
|
|
49
|
+
if (source.includes(legacyCursorPatchMarker) || source.includes("/*ocx-cursor-model-metadata-v2*/") || source.includes("/*ocx-cursor-model-metadata-v3*/") || source.includes("/*ocx-cursor-model-metadata-v4*/")) {
|
|
34
50
|
const matches = [...source.matchAll(previousCatalogInjection)];
|
|
35
51
|
if (matches.length !== 1) {
|
|
36
52
|
throw new Error(`Expected one legacy Cursor model metadata hook, found ${matches.length}`);
|
|
@@ -81,19 +97,103 @@ export function patchCursorBundleSource(source, options = {}) {
|
|
|
81
97
|
};
|
|
82
98
|
}
|
|
83
99
|
|
|
84
|
-
|
|
85
|
-
if (source.includes(cursorLocalRuntimePatchMarker)) return
|
|
100
|
+
function patchCursorLocalRuntimeDisplayName(source) {
|
|
101
|
+
if (source.includes(cursorLocalRuntimePatchMarker)) return source;
|
|
102
|
+
if (source.includes(legacyCursorLocalRuntimePatchMarker)) {
|
|
103
|
+
return source
|
|
104
|
+
.replace(legacyCursorLocalRuntimePatchMarker, cursorLocalRuntimePatchMarker)
|
|
105
|
+
.replace('e.startsWith("opencodex/")?e.split("/")', 'e.startsWith("opencodex/")?(e.startsWith("opencodex/cursor/")?"Cursor ":"")+e.split("/")');
|
|
106
|
+
}
|
|
86
107
|
const matches = [...source.matchAll(localModelConstructor)];
|
|
87
108
|
if (matches.length !== 1) {
|
|
88
109
|
throw new Error(`Expected one Cursor local model constructor, found ${matches.length}`);
|
|
89
110
|
}
|
|
90
111
|
const match = matches[0];
|
|
91
112
|
const { functionName, modelType } = match.groups;
|
|
92
|
-
const replacement = `function ${functionName}(e,t){${cursorLocalRuntimePatchMarker}const ocxCursorDisplayWords={claude:"Claude",codex:"Codex",composer:"Composer",deepseek:"DeepSeek",fable:"Fable",fast:"Fast",flash:"Flash",gpt:"GPT",grok:"Grok",hy3:"HY3",kimi:"Kimi",luna:"Luna",max:"Max",mimo:"MiMo",mini:"Mini",opus:"Opus",pro:"Pro",qwen:"Qwen",sol:"Sol",sonnet:"Sonnet",spark:"Spark",terra:"Terra"};const ocxCursorDisplayName=null!=t?t:e.startsWith("opencodex/")?e.split("/").at(-1).split("-").map(ocxCursorWord=>{if(ocxCursorDisplayWords[ocxCursorWord])return ocxCursorDisplayWords[ocxCursorWord];const ocxCursorAttached=/^(qwen|kimi|gpt|claude|grok)(\\d+(?:\\.\\d+)*)$/.exec(ocxCursorWord);if(ocxCursorAttached)return ocxCursorDisplayWords[ocxCursorAttached[1]]+" "+ocxCursorAttached[2];if(/^v\\d/i.test(ocxCursorWord))return"V"+ocxCursorWord.slice(1);if(/^k\\d/i.test(ocxCursorWord))return"K"+ocxCursorWord.slice(1);if(/^\\d/.test(ocxCursorWord))return ocxCursorWord;return ocxCursorWord.slice(0,1).toUpperCase()+ocxCursorWord.slice(1)}).join(" "):e;return new ${modelType}({modelId:e,displayModelId:e,displayName:ocxCursorDisplayName,displayNameShort:ocxCursorDisplayName,aliases:[]})}`;
|
|
93
|
-
return {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
113
|
+
const replacement = `function ${functionName}(e,t){${cursorLocalRuntimePatchMarker}const ocxCursorDisplayWords={claude:"Claude",codex:"Codex",composer:"Composer",deepseek:"DeepSeek",fable:"Fable",fast:"Fast",flash:"Flash",gpt:"GPT",grok:"Grok",hy3:"HY3",kimi:"Kimi",luna:"Luna",max:"Max",mimo:"MiMo",mini:"Mini",opus:"Opus",pro:"Pro",qwen:"Qwen",sol:"Sol",sonnet:"Sonnet",spark:"Spark",terra:"Terra"};const ocxCursorDisplayName=null!=t?t:e.startsWith("opencodex/")?(e.startsWith("opencodex/cursor/")?"Cursor ":"")+e.split("/").at(-1).split("-").map(ocxCursorWord=>{if(ocxCursorDisplayWords[ocxCursorWord])return ocxCursorDisplayWords[ocxCursorWord];const ocxCursorAttached=/^(qwen|kimi|gpt|claude|grok)(\\d+(?:\\.\\d+)*)$/.exec(ocxCursorWord);if(ocxCursorAttached)return ocxCursorDisplayWords[ocxCursorAttached[1]]+" "+ocxCursorAttached[2];if(/^v\\d/i.test(ocxCursorWord))return"V"+ocxCursorWord.slice(1);if(/^k\\d/i.test(ocxCursorWord))return"K"+ocxCursorWord.slice(1);if(/^\\d/.test(ocxCursorWord))return ocxCursorWord;return ocxCursorWord.slice(0,1).toUpperCase()+ocxCursorWord.slice(1)}).join(" "):e;return new ${modelType}({modelId:e,displayModelId:e,displayName:ocxCursorDisplayName,displayNameShort:ocxCursorDisplayName,aliases:[]})}`;
|
|
114
|
+
return `${source.slice(0, match.index)}${replacement}${source.slice(match.index + match[0].length)}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function patchCursorLocalRuntimeCapabilities(source) {
|
|
118
|
+
source = replaceSingleMatch(source, localRuntimeVisionCapability, (match) => {
|
|
119
|
+
const object = match.groups.object;
|
|
120
|
+
return `${match[0].slice(0, -1)},"boolean"==typeof ${object}.supports_fast?{supports_fast:${object}.supports_fast}:{})`;
|
|
121
|
+
}, "local model capability parser");
|
|
122
|
+
|
|
123
|
+
source = replaceSingleMatch(source, localRuntimePickerInput, (match) => {
|
|
124
|
+
const capabilities = match.groups.capabilities;
|
|
125
|
+
return match[0].replace(
|
|
126
|
+
",supportsVision:",
|
|
127
|
+
`,reasoningEfforts:Array.isArray(${capabilities}?.reasoning_effort)?${capabilities}.reasoning_effort:void 0,supportsFast:!0===${capabilities}?.supports_fast,supportsVision:`,
|
|
128
|
+
);
|
|
129
|
+
}, "local picker input mapper");
|
|
130
|
+
|
|
131
|
+
source = patchCursorLocalRuntimeProviderCapabilities(source);
|
|
132
|
+
|
|
133
|
+
source = replaceSingleMatch(source, localRuntimeRequestParameters, (match) => {
|
|
134
|
+
const { payload, parameters } = match.groups;
|
|
135
|
+
return `${match[0]}const ocxCursorFast=${parameters}?.find(ocxCursorParameter=>ocxCursorParameter.id==="fast")?.value;if(ocxCursorFast==="true")${payload}.service_tier="priority";else if(ocxCursorFast==="false")delete ${payload}.service_tier;`;
|
|
136
|
+
}, "local request parameter mapper");
|
|
137
|
+
|
|
138
|
+
const exportMatches = [...source.matchAll(localRuntimeBuilderExport)];
|
|
139
|
+
if (exportMatches.length !== 1) {
|
|
140
|
+
throw new Error(`Expected one Cursor local picker builder export, found ${exportMatches.length}`);
|
|
141
|
+
}
|
|
142
|
+
const builderName = exportMatches[0].groups.builder;
|
|
143
|
+
const builderStart = source.indexOf(`function ${builderName}(`, exportMatches[0].index);
|
|
144
|
+
if (builderStart === -1) throw new Error("Cursor local picker builder was not found");
|
|
145
|
+
const builderHead = source.slice(builderStart, builderStart + 1_000);
|
|
146
|
+
const modelBuilderName = /\.push\((?<name>[A-Za-z_$][\w$]*)\(/.exec(builderHead)?.groups.name;
|
|
147
|
+
if (!modelBuilderName) throw new Error("Cursor local picker model builder was not found");
|
|
148
|
+
|
|
149
|
+
const modelBuilderStart = source.indexOf(`function ${modelBuilderName}(`, builderStart);
|
|
150
|
+
const modelBuilderSource = source.slice(modelBuilderStart, modelBuilderStart + 6_000);
|
|
151
|
+
const signature = new RegExp(`function ${modelBuilderName}\\((?<model>[A-Za-z_$][\\w$]*),(?<tier>[A-Za-z_$][\\w$]*)\\)\\{var [^;]+;const (?<effort>[A-Za-z_$][\\w$]*)=function\\((?<inner>[A-Za-z_$][\\w$]*)\\)\\{`).exec(modelBuilderSource);
|
|
152
|
+
if (!signature) throw new Error("Cursor local picker model effort builder was not found");
|
|
153
|
+
const { model, effort } = signature.groups;
|
|
154
|
+
const effortPrefix = `const ${effort}=function(`;
|
|
155
|
+
const effortIndex = modelBuilderSource.indexOf(effortPrefix);
|
|
156
|
+
const effortReplacement = `const ${effort}=Array.isArray(${model}.reasoningEfforts)&&${model}.reasoningEfforts.length>0?{param:"reasoning_effort",values:${model}.reasoningEfforts,defaultValue:${model}.reasoningEfforts.includes("medium")?"medium":${model}.reasoningEfforts.includes("high")?"high":${model}.reasoningEfforts[0]}:function(`;
|
|
157
|
+
let patchedBuilder = `${modelBuilderSource.slice(0, effortIndex)}${effortReplacement}${modelBuilderSource.slice(effortIndex + effortPrefix.length)}`;
|
|
158
|
+
|
|
159
|
+
const definitions = new RegExp(`(?<name>[A-Za-z_$][\\w$]*)=\\[\\.\\.\\.void 0!==${effort}\\?`).exec(patchedBuilder)?.groups.name;
|
|
160
|
+
const parameterClass = /new (?<name>[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?)\(\{id:"reasoning"/.exec(patchedBuilder)?.groups.name;
|
|
161
|
+
if (!definitions || !parameterClass) throw new Error("Cursor local picker parameter definition builder was not found");
|
|
162
|
+
const definitionsEnd = patchedBuilder.indexOf("];var ", patchedBuilder.indexOf(`${definitions}=[`));
|
|
163
|
+
if (definitionsEnd === -1) throw new Error("Cursor local picker parameter definitions were not terminated");
|
|
164
|
+
const fastDefinition = `];${model}.supportsFast&&${definitions}.push(new ${parameterClass}({id:"fast",name:"Fast",markdownTooltip:"Use priority processing with increased usage.",parameterType:{booleanParameter:{values:[{value:"false"},{value:"true",displayName:"Fast",increasesModelCost:!0}]}},isCycleableByHotkey:!1}))`;
|
|
165
|
+
patchedBuilder = `${patchedBuilder.slice(0, definitionsEnd)}${fastDefinition}${patchedBuilder.slice(definitionsEnd + 1)}`;
|
|
166
|
+
|
|
167
|
+
const variantMatch = new RegExp(`variants:(?<call>[A-Za-z_$][\\w$]*\\(${effort},(?<context>[A-Za-z_$][\\w$]*),(?<display>[A-Za-z_$][\\w$]*),(?<tier>[A-Za-z_$][\\w$]*)\\))`).exec(patchedBuilder);
|
|
168
|
+
if (!variantMatch) throw new Error("Cursor local picker variant builder was not found");
|
|
169
|
+
const variantReplacement = `variants:ocxCursorFastVariants(${variantMatch.groups.call},${model}.supportsFast,${variantMatch.groups.display})`;
|
|
170
|
+
patchedBuilder = `${patchedBuilder.slice(0, variantMatch.index)}${variantReplacement}${patchedBuilder.slice(variantMatch.index + variantMatch[0].length)}`;
|
|
171
|
+
|
|
172
|
+
source = `${source.slice(0, modelBuilderStart)}${patchedBuilder}${source.slice(modelBuilderStart + modelBuilderSource.length)}`;
|
|
173
|
+
const helper = `${cursorLocalRuntimeCapabilitiesPatchMarker}function ocxCursorFastVariants(e,t,n){if(!t)return e;const r=e.length>0?e:[{parameterValues:[],displayName:n,displayNameOutsidePicker:n,isMaxMode:!1,isDefaultNonMaxConfig:!0,isDefaultMaxConfig:!0}];return r.flatMap(e=>[{...e,parameterValues:[...(e.parameterValues??[]),{id:"fast",value:"false"}]},{...e,parameterValues:[...(e.parameterValues??[]),{id:"fast",value:"true"}],displayName:e.displayName+" Fast",displayNameOutsidePicker:(e.displayNameOutsidePicker??e.displayName)+" Fast",isDefaultNonMaxConfig:!1,isDefaultMaxConfig:!1}])}`;
|
|
174
|
+
return `${source.slice(0, builderStart)}${helper}${source.slice(builderStart)}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function patchCursorLocalRuntimeProviderCapabilities(source) {
|
|
178
|
+
return replaceSingleMatch(source, localRuntimeProviderPickerInput, (match) => {
|
|
179
|
+
const { model, reasoning } = match.groups;
|
|
180
|
+
return `supportsReasoning:${reasoning},reasoningEfforts:Array.isArray(${model}.capabilities?.reasoning_effort)?${model}.capabilities.reasoning_effort:void 0,supportsFast:!0===${model}.capabilities?.supports_fast,supportsVision:`;
|
|
181
|
+
}, "standalone local provider picker mapper");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function patchCursorLocalRuntimeSource(source) {
|
|
185
|
+
const displayPatched = patchCursorLocalRuntimeDisplayName(source);
|
|
186
|
+
if (displayPatched.includes(cursorLocalRuntimeCapabilitiesPatchMarker)) {
|
|
187
|
+
return { status: displayPatched === source ? "already-patched" : "patched", source: displayPatched };
|
|
188
|
+
}
|
|
189
|
+
if (displayPatched.includes(legacyCursorLocalRuntimeCapabilitiesPatchMarker)) {
|
|
190
|
+
const upgraded = patchCursorLocalRuntimeProviderCapabilities(displayPatched).replace(
|
|
191
|
+
legacyCursorLocalRuntimeCapabilitiesPatchMarker,
|
|
192
|
+
cursorLocalRuntimeCapabilitiesPatchMarker,
|
|
193
|
+
);
|
|
194
|
+
return { status: "patched", source: upgraded };
|
|
195
|
+
}
|
|
196
|
+
return { status: "patched", source: patchCursorLocalRuntimeCapabilities(displayPatched) };
|
|
97
197
|
}
|
|
98
198
|
|
|
99
199
|
export async function cursorWorkbenchSignature(file = cursorWorkbenchFile) {
|
package/src/cursor-state.mjs
CHANGED
|
@@ -42,12 +42,14 @@ function displayWord(value) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export function displayNameFor(model) {
|
|
45
|
-
|
|
45
|
+
const displayName = model.sourceId.split("/").at(-1).split("-").map(displayWord).join(" ");
|
|
46
|
+
return model.provider === "cursor" || model.sourceId.startsWith("cursor/") ? `Cursor ${displayName}` : displayName;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export function cursorIsRunning() {
|
|
49
50
|
try {
|
|
50
|
-
|
|
51
|
+
// Match the main binary only: orphaned crashpad helpers under Contents/ outlive the app.
|
|
52
|
+
execFileSync("pgrep", ["-f", "/Applications/Cursor.app/Contents/MacOS/Cursor"], { stdio: "ignore" });
|
|
51
53
|
return true;
|
|
52
54
|
} catch {
|
|
53
55
|
return false;
|
|
@@ -165,6 +167,9 @@ function syncSelectedModel(state, catalog) {
|
|
|
165
167
|
const composer = state.aiSettings?.modelConfig?.composer;
|
|
166
168
|
if (!composer || !Array.isArray(composer.selectedModels)) return;
|
|
167
169
|
const byAlias = new Map(catalog.map((model) => [model.alias, model]));
|
|
170
|
+
composer.selectedModels = composer.selectedModels.filter((selected) => (
|
|
171
|
+
typeof selected?.modelId !== "string" || !selected.modelId.startsWith(managedPrefix) || byAlias.has(selected.modelId)
|
|
172
|
+
));
|
|
168
173
|
for (const selected of composer.selectedModels) {
|
|
169
174
|
const model = byAlias.get(selected.modelId);
|
|
170
175
|
if (!model || (model.reasoningEfforts.length === 0 && !model.supportsFast)) continue;
|
|
@@ -187,7 +192,7 @@ export function applyCatalogToState(state, catalog) {
|
|
|
187
192
|
const keepUnmanaged = (value) => typeof value !== "string" || !value.startsWith(managedPrefix);
|
|
188
193
|
const existingModels = Array.isArray(state.availableDefaultModels2) ? state.availableDefaultModels2 : [];
|
|
189
194
|
state.availableDefaultModels2 = [
|
|
190
|
-
...existingModels.filter((model) => !(
|
|
195
|
+
...existingModels.filter((model) => !(typeof model?.name === "string" && model.name.startsWith(managedPrefix))),
|
|
191
196
|
...catalog.map(cursorModel),
|
|
192
197
|
];
|
|
193
198
|
state.aiSettings ||= {};
|
|
@@ -199,6 +204,14 @@ export function applyCatalogToState(state, catalog) {
|
|
|
199
204
|
...(Array.isArray(state.aiSettings.modelOverrideEnabled) ? state.aiSettings.modelOverrideEnabled.filter(keepUnmanaged) : []),
|
|
200
205
|
...aliases,
|
|
201
206
|
];
|
|
207
|
+
if (state.aiSettings.modelParameterPreferences && typeof state.aiSettings.modelParameterPreferences === "object") {
|
|
208
|
+
const activeAliases = new Set(aliases);
|
|
209
|
+
for (const alias of Object.keys(state.aiSettings.modelParameterPreferences)) {
|
|
210
|
+
if (alias.startsWith(managedPrefix) && !activeAliases.has(alias)) {
|
|
211
|
+
delete state.aiSettings.modelParameterPreferences[alias];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
202
215
|
syncSelectedModel(state, catalog);
|
|
203
216
|
return state;
|
|
204
217
|
}
|
package/src/gateway.mjs
CHANGED
|
@@ -118,10 +118,15 @@ async function serviceToken() {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
function authorized(req, expected) {
|
|
122
|
-
const supplied =
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
export function authorized(req, expected) {
|
|
122
|
+
const supplied = [
|
|
123
|
+
String(req.headers.authorization || "").replace(/^Bearer\s+/i, "").trim(),
|
|
124
|
+
String(req.headers["x-api-key"] || "").trim(),
|
|
125
|
+
];
|
|
126
|
+
return supplied.some((value) => {
|
|
127
|
+
const actual = Buffer.from(value);
|
|
128
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
129
|
+
});
|
|
125
130
|
}
|
|
126
131
|
|
|
127
132
|
function json(res, status, body) {
|
|
@@ -161,7 +166,7 @@ function upstreamHeaders(req, body, token, upstream) {
|
|
|
161
166
|
|
|
162
167
|
export function startGateway(options) {
|
|
163
168
|
const host = options.host || gatewayHost;
|
|
164
|
-
const port = options.port
|
|
169
|
+
const port = options.port ?? gatewayPort;
|
|
165
170
|
const expected = Buffer.from(options.secret);
|
|
166
171
|
let activeRequests = 0;
|
|
167
172
|
|
|
@@ -179,16 +184,29 @@ export function startGateway(options) {
|
|
|
179
184
|
if (activeRequests >= 8) return json(res, 429, { error: { message: "bridge concurrency limit reached", type: "rate_limit_error" } });
|
|
180
185
|
|
|
181
186
|
activeRequests += 1;
|
|
182
|
-
|
|
187
|
+
let upstreamReq;
|
|
188
|
+
let downstreamClosed = false;
|
|
189
|
+
const cancelUpstream = () => {
|
|
190
|
+
downstreamClosed = true;
|
|
191
|
+
if (upstreamReq && !upstreamReq.destroyed) upstreamReq.destroy();
|
|
192
|
+
};
|
|
193
|
+
req.once("aborted", cancelUpstream);
|
|
194
|
+
res.once("close", () => {
|
|
195
|
+
activeRequests = Math.max(0, activeRequests - 1);
|
|
196
|
+
if (!res.writableEnded) cancelUpstream();
|
|
197
|
+
});
|
|
183
198
|
try {
|
|
184
199
|
if (route === "GET /v1/models") {
|
|
185
200
|
return json(res, 200, enrichModelList(await activeModels(), catalog));
|
|
186
201
|
}
|
|
187
202
|
|
|
188
|
-
const upstream = opencodexEndpoint();
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
const
|
|
203
|
+
const upstream = options.upstream || opencodexEndpoint();
|
|
204
|
+
const receivedBody = await readBody(req);
|
|
205
|
+
if (downstreamClosed) return;
|
|
206
|
+
const body = rewriteModelAliasBody(receivedBody, catalog);
|
|
207
|
+
const token = options.serviceToken !== undefined ? options.serviceToken : await serviceToken();
|
|
208
|
+
if (downstreamClosed) return;
|
|
209
|
+
upstreamReq = http.request({
|
|
192
210
|
hostname: upstream.host,
|
|
193
211
|
port: upstream.port,
|
|
194
212
|
method: req.method,
|
|
@@ -204,6 +222,7 @@ export function startGateway(options) {
|
|
|
204
222
|
});
|
|
205
223
|
});
|
|
206
224
|
upstreamReq.on("error", (error) => {
|
|
225
|
+
if (downstreamClosed) return;
|
|
207
226
|
if (!res.headersSent) json(res, 502, { error: { message: "OpenCodex upstream unavailable", type: "upstream_error" } });
|
|
208
227
|
else res.destroy(error);
|
|
209
228
|
});
|