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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocx-cursor",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "OpenCodex companion service for Cursor custom models",
5
5
  "keywords": [
6
6
  "opencodex",
@@ -9,11 +9,12 @@ import {
9
9
  cursorWorkbenchFile,
10
10
  } from "./paths.mjs";
11
11
 
12
- export const cursorPatchMarker = "/*ocx-cursor-model-metadata-v4*/";
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[23])?\*\/(?<catalog>[A-Za-z_$][\w$]*)=\k<catalog>\.map\(ocxCursorModel=>\{.*?\}\),(?=(?<batch>[A-Za-z_$][\w$]*)\(\(\)=>\{this\._reactiveStorageService\.setApplicationUserPersistentStorage\("availableDefaultModels2",\k<catalog>\))/g;
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
- if (source.includes(cursorLocalRuntimeCapabilitiesPatchMarker)) return { status: "already-patched", source };
180
- if (source.includes(legacyCursorLocalRuntimeCapabilitiesPatchMarker)) {
181
- const upgraded = patchCursorLocalRuntimeProviderCapabilities(source).replace(
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
 
@@ -42,12 +42,14 @@ function displayWord(value) {
42
42
  }
43
43
 
44
44
  export function displayNameFor(model) {
45
- return model.sourceId.split("/").at(-1).split("-").map(displayWord).join(" ");
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
- execFileSync("pgrep", ["-f", "/Applications/Cursor.app/Contents/"], { stdio: "ignore" });
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) => !(model?.isUserAdded && typeof model.name === "string" && model.name.startsWith(managedPrefix))),
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 = String(req.headers.authorization || "").replace(/^Bearer\s+/i, "").trim();
123
- const actual = Buffer.from(supplied);
124
- return actual.length === expected.length && timingSafeEqual(actual, expected);
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 || gatewayPort;
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
- res.once("close", () => { activeRequests = Math.max(0, activeRequests - 1); });
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 body = rewriteModelAliasBody(await readBody(req), catalog);
190
- const token = await serviceToken();
191
- const upstreamReq = http.request({
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
  });