ocx-cursor 0.5.1 → 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 +2 -0
- package/package.json +1 -1
- package/src/cursor-patch.mjs +18 -10
- package/src/cursor-state.mjs +16 -3
- package/src/gateway.mjs +29 -10
package/README.md
CHANGED
|
@@ -59,6 +59,8 @@ Cursor -> 127.0.0.1:10101 -> OpenCodex on 127.0.0.1:10100 -> provider
|
|
|
59
59
|
|
|
60
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`.
|
|
61
61
|
|
|
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.
|
|
63
|
+
|
|
62
64
|
## Model behavior
|
|
63
65
|
|
|
64
66
|
The bridge reads OpenCodex's active `/v1/models` response and creates stable Cursor aliases:
|
package/package.json
CHANGED
package/src/cursor-patch.mjs
CHANGED
|
@@ -9,11 +9,12 @@ 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*/";
|
|
17
18
|
export const cursorLocalRuntimeCapabilitiesPatchMarker = "/*ocx-cursor-local-model-capabilities-v3*/";
|
|
18
19
|
export const legacyCursorLocalRuntimeCapabilitiesPatchMarker = "/*ocx-cursor-local-model-capabilities-v2*/";
|
|
19
20
|
|
|
@@ -22,7 +23,7 @@ export function isCursorModelMetadataBundle(file) {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
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;
|
|
25
|
-
const previousCatalogInjection = /\/\*ocx-cursor-model-metadata(?:-v[
|
|
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;
|
|
26
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;
|
|
27
28
|
const localRuntimeVisionCapability = /"boolean"==typeof (?<object>[A-Za-z_$][\w$]*)\.supports_vision\?\{supports_vision:\k<object>\.supports_vision\}:\{\}\)/g;
|
|
28
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;
|
|
@@ -40,12 +41,12 @@ function replaceSingleMatch(source, pattern, replacement, label) {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
function metadataInjection(catalog) {
|
|
43
|
-
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 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}}),`;
|
|
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}}),`;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
export function patchCursorWorkbenchSource(source) {
|
|
47
48
|
if (source.includes(cursorPatchMarker)) return { status: "already-patched", source };
|
|
48
|
-
if (source.includes(legacyCursorPatchMarker) || source.includes("/*ocx-cursor-model-metadata-v2*/") || source.includes("/*ocx-cursor-model-metadata-v3*/")) {
|
|
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*/")) {
|
|
49
50
|
const matches = [...source.matchAll(previousCatalogInjection)];
|
|
50
51
|
if (matches.length !== 1) {
|
|
51
52
|
throw new Error(`Expected one legacy Cursor model metadata hook, found ${matches.length}`);
|
|
@@ -98,13 +99,18 @@ export function patchCursorBundleSource(source, options = {}) {
|
|
|
98
99
|
|
|
99
100
|
function patchCursorLocalRuntimeDisplayName(source) {
|
|
100
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
|
+
}
|
|
101
107
|
const matches = [...source.matchAll(localModelConstructor)];
|
|
102
108
|
if (matches.length !== 1) {
|
|
103
109
|
throw new Error(`Expected one Cursor local model constructor, found ${matches.length}`);
|
|
104
110
|
}
|
|
105
111
|
const match = matches[0];
|
|
106
112
|
const { functionName, modelType } = match.groups;
|
|
107
|
-
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:[]})}`;
|
|
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:[]})}`;
|
|
108
114
|
return `${source.slice(0, match.index)}${replacement}${source.slice(match.index + match[0].length)}`;
|
|
109
115
|
}
|
|
110
116
|
|
|
@@ -176,15 +182,17 @@ function patchCursorLocalRuntimeProviderCapabilities(source) {
|
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
export function patchCursorLocalRuntimeSource(source) {
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
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(
|
|
182
191
|
legacyCursorLocalRuntimeCapabilitiesPatchMarker,
|
|
183
192
|
cursorLocalRuntimeCapabilitiesPatchMarker,
|
|
184
193
|
);
|
|
185
194
|
return { status: "patched", source: upgraded };
|
|
186
195
|
}
|
|
187
|
-
const displayPatched = patchCursorLocalRuntimeDisplayName(source);
|
|
188
196
|
return { status: "patched", source: patchCursorLocalRuntimeCapabilities(displayPatched) };
|
|
189
197
|
}
|
|
190
198
|
|
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
|
});
|