cursor-openai-byok 1.0.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 +252 -0
- package/TROUBLESHOOTING.md +84 -0
- package/bin/cursor-openai-byok.js +9 -0
- package/config.example.json +48 -0
- package/lib/cli.js +1888 -0
- package/lib/daemon-entry.js +1128 -0
- package/lib/extension.js +302 -0
- package/package.json +77 -0
- package/scripts/install.ps1 +3 -0
- package/scripts/install.sh +4 -0
- package/scripts/uninstall.ps1 +3 -0
- package/scripts/uninstall.sh +4 -0
|
@@ -0,0 +1,1128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const __bundlePath = require("path");
|
|
5
|
+
const __bundleRoot = __bundlePath.resolve(__dirname, "..");
|
|
6
|
+
const __bundleModules = {
|
|
7
|
+
"src/daemon-entry.js": function(module, exports, require, __filename, __dirname) {
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const { startServer } = require("./daemon/server");
|
|
11
|
+
const { pidPath } = require("./paths");
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
|
|
14
|
+
fs.writeFileSync(pidPath(), String(process.pid));
|
|
15
|
+
const server = startServer();
|
|
16
|
+
|
|
17
|
+
function shutdown() {
|
|
18
|
+
try { fs.unlinkSync(pidPath()); } catch {}
|
|
19
|
+
server.close(() => process.exit(0));
|
|
20
|
+
setTimeout(() => process.exit(0), 1000).unref();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
process.on("SIGTERM", shutdown);
|
|
24
|
+
process.on("SIGINT", shutdown);
|
|
25
|
+
process.on("exit", () => {
|
|
26
|
+
try { fs.unlinkSync(pidPath()); } catch {}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
},
|
|
30
|
+
"src/daemon/server.js": function(module, exports, require, __filename, __dirname) {
|
|
31
|
+
"use strict";
|
|
32
|
+
|
|
33
|
+
const http = require("http");
|
|
34
|
+
const { loadConfig, listModels, resolveModel } = require("../config");
|
|
35
|
+
const { log } = require("../logger");
|
|
36
|
+
const { DEFAULT_PORT } = require("../paths");
|
|
37
|
+
const { cursorPayloadToOpenAI } = require("../cursor-agent-adapter/messages");
|
|
38
|
+
const {
|
|
39
|
+
proxyOpenAIRequest,
|
|
40
|
+
streamProviderEvents,
|
|
41
|
+
callProviderOnce,
|
|
42
|
+
} = require("../provider-adapters/openai-compatible");
|
|
43
|
+
|
|
44
|
+
function readJson(req, limit = 30 * 1024 * 1024) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
let body = "";
|
|
47
|
+
req.setEncoding("utf8");
|
|
48
|
+
req.on("data", (chunk) => {
|
|
49
|
+
body += chunk;
|
|
50
|
+
if (body.length > limit) {
|
|
51
|
+
reject(new Error("request too large"));
|
|
52
|
+
req.destroy();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
req.on("end", () => {
|
|
56
|
+
try {
|
|
57
|
+
resolve(body ? JSON.parse(body) : {});
|
|
58
|
+
} catch (err) {
|
|
59
|
+
reject(err);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
req.on("error", reject);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sendJson(res, status, body) {
|
|
67
|
+
res.writeHead(status, corsHeaders({ "content-type": "application/json; charset=utf-8" }));
|
|
68
|
+
res.end(JSON.stringify(body));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function corsHeaders(extra = {}) {
|
|
72
|
+
return {
|
|
73
|
+
"access-control-allow-origin": "*",
|
|
74
|
+
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
75
|
+
"access-control-allow-headers": "content-type,authorization,openai-beta",
|
|
76
|
+
...extra,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function pipeProviderResponse(res, providerResponse) {
|
|
81
|
+
const headers = {
|
|
82
|
+
"content-type": providerResponse.headers.get("content-type") || "application/json; charset=utf-8",
|
|
83
|
+
};
|
|
84
|
+
res.writeHead(providerResponse.status, corsHeaders(headers));
|
|
85
|
+
if (!providerResponse.body) {
|
|
86
|
+
res.end(await providerResponse.text());
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
for await (const chunk of providerResponse.body) res.write(chunk);
|
|
90
|
+
res.end();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function sendNdjson(res, iterator) {
|
|
94
|
+
res.writeHead(200, corsHeaders({ "content-type": "application/x-ndjson; charset=utf-8" }));
|
|
95
|
+
try {
|
|
96
|
+
for await (const event of iterator) {
|
|
97
|
+
res.write(JSON.stringify(event) + "\n");
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const message = err && err.message ? err.message : String(err);
|
|
101
|
+
log("stream-error", { error: err && err.stack ? err.stack : message });
|
|
102
|
+
res.write(JSON.stringify({ type: "error", error: message }) + "\n");
|
|
103
|
+
}
|
|
104
|
+
res.write(JSON.stringify({ type: "done" }) + "\n");
|
|
105
|
+
res.end();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function* runCursorAgentLoop(config, payload) {
|
|
109
|
+
const requestedModel = payload.model || payload.modelName || payload?.runRequest?.requestedModel?.modelId;
|
|
110
|
+
const route = resolveModel(config, requestedModel);
|
|
111
|
+
const openaiBody = cursorPayloadToOpenAI(payload, route);
|
|
112
|
+
log("cursor-run", {
|
|
113
|
+
model: route.model.displayName,
|
|
114
|
+
provider: route.provider.id,
|
|
115
|
+
conversationId: payload.conversationId,
|
|
116
|
+
generationUUID: payload.generationUUID,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (route.model.supportsStreaming === false) {
|
|
120
|
+
const text = await callProviderOnce(config, route, openaiBody);
|
|
121
|
+
if (text) yield { type: "text_delta", text };
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const pendingTools = new Map();
|
|
126
|
+
for await (const event of streamProviderEvents(config, route, openaiBody)) {
|
|
127
|
+
if (event.type === "tool_call_delta") {
|
|
128
|
+
const key = event.id || String(event.index || pendingTools.size);
|
|
129
|
+
const existing = pendingTools.get(key) || { id: key, name: event.name, arguments: "" };
|
|
130
|
+
existing.name = event.name || existing.name;
|
|
131
|
+
existing.arguments += event.argumentsDelta || "";
|
|
132
|
+
pendingTools.set(key, existing);
|
|
133
|
+
yield event;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (event.type === "tool_call") {
|
|
137
|
+
pendingTools.set(event.id, event);
|
|
138
|
+
yield event;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (event.type === "finish" && pendingTools.size > 0) {
|
|
142
|
+
yield { type: "tool_calls_ready", toolCalls: [...pendingTools.values()] };
|
|
143
|
+
pendingTools.clear();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
yield event;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function createServer() {
|
|
151
|
+
return http.createServer(async (req, res) => {
|
|
152
|
+
try {
|
|
153
|
+
if (req.method === "OPTIONS") return sendJson(res, 204, {});
|
|
154
|
+
const config = loadConfig();
|
|
155
|
+
const url = new URL(req.url, "http://127.0.0.1");
|
|
156
|
+
|
|
157
|
+
if (req.method === "GET" && url.pathname === "/health") {
|
|
158
|
+
return sendJson(res, 200, {
|
|
159
|
+
ok: true,
|
|
160
|
+
app: "cursor-openai-byok",
|
|
161
|
+
models: listModels(config).map((m) => m.displayName),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (req.method === "GET" && url.pathname === "/cursor/v1/models") {
|
|
166
|
+
return sendJson(res, 200, { ok: true, models: listModels(config) });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (req.method === "GET" && url.pathname === "/cursor/v1/should-handle") {
|
|
170
|
+
const model = url.searchParams.get("model");
|
|
171
|
+
try {
|
|
172
|
+
const route = resolveModel(config, model);
|
|
173
|
+
return sendJson(res, 200, { ok: true, handle: true, model: route.model });
|
|
174
|
+
} catch {
|
|
175
|
+
return sendJson(res, 200, { ok: true, handle: false });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (req.method === "POST" && url.pathname === "/cursor/v1/run-agent-loop") {
|
|
180
|
+
const payload = await readJson(req);
|
|
181
|
+
return sendNdjson(res, runCursorAgentLoop(config, payload));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (req.method === "POST" && url.pathname === "/cursor/v1/tool-result") {
|
|
185
|
+
const payload = await readJson(req);
|
|
186
|
+
log("tool-result", {
|
|
187
|
+
conversationId: payload.conversationId,
|
|
188
|
+
callId: payload.callId,
|
|
189
|
+
ok: payload.ok !== false,
|
|
190
|
+
});
|
|
191
|
+
return sendJson(res, 200, { ok: true });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (req.method === "POST" && url.pathname === "/v1/responses") {
|
|
195
|
+
const body = await readJson(req);
|
|
196
|
+
return pipeProviderResponse(res, await proxyOpenAIRequest(config, "responses", body, req.headers));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (req.method === "POST" && url.pathname === "/v1/chat/completions") {
|
|
200
|
+
const body = await readJson(req);
|
|
201
|
+
return pipeProviderResponse(res, await proxyOpenAIRequest(config, "chat", body, req.headers));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
sendJson(res, 404, { ok: false, error: "not found" });
|
|
205
|
+
} catch (err) {
|
|
206
|
+
log("error", { url: req.url, error: err && err.stack ? err.stack : String(err) });
|
|
207
|
+
sendJson(res, 500, { ok: false, error: err && err.message ? err.message : String(err) });
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function startServer(options = {}) {
|
|
213
|
+
const config = loadConfig();
|
|
214
|
+
const host = options.host || config.server?.host || "127.0.0.1";
|
|
215
|
+
const port = Number(options.port || config.server?.port || DEFAULT_PORT);
|
|
216
|
+
const server = createServer();
|
|
217
|
+
server.listen(port, host, () => {
|
|
218
|
+
log("listening", { host, port });
|
|
219
|
+
console.log(`cursor-openai-byok listening on http://${host}:${port}`);
|
|
220
|
+
});
|
|
221
|
+
return server;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = { createServer, startServer, runCursorAgentLoop, readJson };
|
|
225
|
+
|
|
226
|
+
},
|
|
227
|
+
"src/config.js": function(module, exports, require, __filename, __dirname) {
|
|
228
|
+
"use strict";
|
|
229
|
+
|
|
230
|
+
const fs = require("fs");
|
|
231
|
+
const path = require("path");
|
|
232
|
+
const { configDir, configPath, DEFAULT_PORT } = require("./paths");
|
|
233
|
+
|
|
234
|
+
const DEFAULT_CONFIG = {
|
|
235
|
+
$schemaVersion: 1,
|
|
236
|
+
server: {
|
|
237
|
+
host: "127.0.0.1",
|
|
238
|
+
port: DEFAULT_PORT,
|
|
239
|
+
},
|
|
240
|
+
defaults: {
|
|
241
|
+
model: "fastskills-gpt-5.5",
|
|
242
|
+
},
|
|
243
|
+
providers: [
|
|
244
|
+
{
|
|
245
|
+
id: "fastskills",
|
|
246
|
+
name: "FastSkills",
|
|
247
|
+
type: "openai-responses",
|
|
248
|
+
baseUrl: "https://api-hw.fastskills.ai/v1",
|
|
249
|
+
apiKey: "sk-REPLACE_ME",
|
|
250
|
+
models: [
|
|
251
|
+
{
|
|
252
|
+
displayName: "fastskills-gpt-5.5",
|
|
253
|
+
apiModel: "gpt-5.5",
|
|
254
|
+
supportsTools: true,
|
|
255
|
+
supportsStreaming: true,
|
|
256
|
+
supportsImages: true,
|
|
257
|
+
contextTokenLimit: 258000,
|
|
258
|
+
maxOutputTokens: 32000,
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
function ensureConfigDir() {
|
|
266
|
+
fs.mkdirSync(configDir(), { recursive: true });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function writeDefaultConfig(target = configPath()) {
|
|
270
|
+
ensureConfigDir();
|
|
271
|
+
if (!fs.existsSync(target)) {
|
|
272
|
+
fs.writeFileSync(target, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n", { mode: 0o600 });
|
|
273
|
+
}
|
|
274
|
+
return target;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function readJsonFile(file) {
|
|
278
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function loadConfig(file = configPath()) {
|
|
282
|
+
if (!fs.existsSync(file)) writeDefaultConfig(file);
|
|
283
|
+
const config = readJsonFile(file);
|
|
284
|
+
validateConfig(config, file);
|
|
285
|
+
return config;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function validateConfig(config, file = configPath()) {
|
|
289
|
+
if (!config || typeof config !== "object") throw new Error(`${file}: config must be an object`);
|
|
290
|
+
if (!Array.isArray(config.providers) || config.providers.length === 0) {
|
|
291
|
+
throw new Error(`${file}: providers must be a non-empty array`);
|
|
292
|
+
}
|
|
293
|
+
const modelNames = new Set();
|
|
294
|
+
for (const provider of config.providers) {
|
|
295
|
+
if (!provider.id || typeof provider.id !== "string") throw new Error(`${file}: provider.id is required`);
|
|
296
|
+
if (!["openai-responses", "openai-chat-completions"].includes(provider.type)) {
|
|
297
|
+
throw new Error(`${file}: provider ${provider.id} type must be openai-responses or openai-chat-completions`);
|
|
298
|
+
}
|
|
299
|
+
if (!provider.baseUrl || typeof provider.baseUrl !== "string") {
|
|
300
|
+
throw new Error(`${file}: provider ${provider.id} baseUrl is required`);
|
|
301
|
+
}
|
|
302
|
+
const key = resolveApiKey(provider, false);
|
|
303
|
+
if (!key || key === "sk-REPLACE_ME") {
|
|
304
|
+
throw new Error(`${file}: provider ${provider.id} apiKey or apiKeyEnv must be configured`);
|
|
305
|
+
}
|
|
306
|
+
if (!Array.isArray(provider.models) || provider.models.length === 0) {
|
|
307
|
+
throw new Error(`${file}: provider ${provider.id} models must be a non-empty array`);
|
|
308
|
+
}
|
|
309
|
+
for (const model of provider.models) {
|
|
310
|
+
if (!model.displayName || typeof model.displayName !== "string") {
|
|
311
|
+
throw new Error(`${file}: provider ${provider.id} model.displayName is required`);
|
|
312
|
+
}
|
|
313
|
+
if (modelNames.has(model.displayName)) {
|
|
314
|
+
throw new Error(`${file}: duplicate model displayName ${model.displayName}`);
|
|
315
|
+
}
|
|
316
|
+
modelNames.add(model.displayName);
|
|
317
|
+
if (model.apiModel !== undefined && typeof model.apiModel !== "string") {
|
|
318
|
+
throw new Error(`${file}: model ${model.displayName} apiModel must be a string`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function resolveApiKey(provider, throwOnMissing = true) {
|
|
325
|
+
const key = provider.apiKeyEnv ? process.env[provider.apiKeyEnv] : provider.apiKey;
|
|
326
|
+
if ((!key || key === "sk-REPLACE_ME") && throwOnMissing) {
|
|
327
|
+
throw new Error(`Missing API key for provider ${provider.id}`);
|
|
328
|
+
}
|
|
329
|
+
return key;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function listModels(config) {
|
|
333
|
+
const models = [];
|
|
334
|
+
for (const provider of config.providers || []) {
|
|
335
|
+
for (const model of provider.models || []) {
|
|
336
|
+
models.push({
|
|
337
|
+
providerId: provider.id,
|
|
338
|
+
providerName: provider.name || provider.id,
|
|
339
|
+
providerType: provider.type,
|
|
340
|
+
displayName: model.displayName,
|
|
341
|
+
apiModel: model.apiModel || model.displayName,
|
|
342
|
+
supportsTools: model.supportsTools !== false,
|
|
343
|
+
supportsStreaming: model.supportsStreaming !== false,
|
|
344
|
+
supportsImages: model.supportsImages !== false,
|
|
345
|
+
contextTokenLimit: model.contextTokenLimit || 128000,
|
|
346
|
+
maxOutputTokens: model.maxOutputTokens || 8192,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return models;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function resolveModel(config, requestedModel) {
|
|
354
|
+
const models = listModels(config);
|
|
355
|
+
const fallback = config.defaults && config.defaults.model;
|
|
356
|
+
const target = requestedModel || fallback || (models[0] && models[0].displayName);
|
|
357
|
+
const found = models.find((m) => m.displayName === target || m.apiModel === target);
|
|
358
|
+
if (!found) {
|
|
359
|
+
throw new Error(`Model ${target || "<empty>"} is not configured`);
|
|
360
|
+
}
|
|
361
|
+
const provider = config.providers.find((p) => p.id === found.providerId);
|
|
362
|
+
return { provider, model: found };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function maskSecret(value) {
|
|
366
|
+
if (!value || typeof value !== "string") return value;
|
|
367
|
+
if (value.length <= 8) return "***";
|
|
368
|
+
return `${value.slice(0, 3)}***${value.slice(-4)}`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function redactConfig(config) {
|
|
372
|
+
return {
|
|
373
|
+
...config,
|
|
374
|
+
providers: (config.providers || []).map((provider) => ({
|
|
375
|
+
...provider,
|
|
376
|
+
apiKey: provider.apiKey ? maskSecret(provider.apiKey) : undefined,
|
|
377
|
+
})),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function bundledTemplatePath() {
|
|
382
|
+
return path.resolve(__dirname, "..", "config.example.json");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
module.exports = {
|
|
386
|
+
DEFAULT_CONFIG,
|
|
387
|
+
ensureConfigDir,
|
|
388
|
+
writeDefaultConfig,
|
|
389
|
+
loadConfig,
|
|
390
|
+
validateConfig,
|
|
391
|
+
resolveApiKey,
|
|
392
|
+
resolveModel,
|
|
393
|
+
listModels,
|
|
394
|
+
redactConfig,
|
|
395
|
+
bundledTemplatePath,
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
},
|
|
399
|
+
"src/paths.js": function(module, exports, require, __filename, __dirname) {
|
|
400
|
+
"use strict";
|
|
401
|
+
|
|
402
|
+
const os = require("os");
|
|
403
|
+
const path = require("path");
|
|
404
|
+
const fs = require("fs");
|
|
405
|
+
|
|
406
|
+
const APP_NAME = "cursor-openai-byok";
|
|
407
|
+
const DEFAULT_PORT = 39832;
|
|
408
|
+
|
|
409
|
+
function homeDir() {
|
|
410
|
+
return os.homedir();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function configDir() {
|
|
414
|
+
return path.join(homeDir(), `.${APP_NAME}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function configPath() {
|
|
418
|
+
return path.join(configDir(), "config.json");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function logPath() {
|
|
422
|
+
return path.join(configDir(), "daemon.log");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function pidPath() {
|
|
426
|
+
return path.join(configDir(), "daemon.pid");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function runtimePath(name) {
|
|
430
|
+
return path.join(configDir(), name);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function platformName() {
|
|
434
|
+
if (process.platform === "darwin") return "macos";
|
|
435
|
+
if (process.platform === "win32") return "windows";
|
|
436
|
+
return process.platform;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function cursorExtensionsDir() {
|
|
440
|
+
return path.join(homeDir(), ".cursor", "extensions");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function installedExtensionDir() {
|
|
444
|
+
const version = require("../package.json").version;
|
|
445
|
+
return path.join(cursorExtensionsDir(), `cursor-openai-byok.cursor-openai-byok-${version}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function ensureDir(dir) {
|
|
449
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
module.exports = {
|
|
453
|
+
APP_NAME,
|
|
454
|
+
DEFAULT_PORT,
|
|
455
|
+
homeDir,
|
|
456
|
+
configDir,
|
|
457
|
+
configPath,
|
|
458
|
+
logPath,
|
|
459
|
+
pidPath,
|
|
460
|
+
runtimePath,
|
|
461
|
+
platformName,
|
|
462
|
+
cursorExtensionsDir,
|
|
463
|
+
installedExtensionDir,
|
|
464
|
+
ensureDir,
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
},
|
|
468
|
+
"package.json": function(module, exports, require, __filename, __dirname) {
|
|
469
|
+
module.exports = {
|
|
470
|
+
"name": "cursor-openai-byok",
|
|
471
|
+
"version": "1.0.2",
|
|
472
|
+
"description": "Local BYOK bridge for Cursor using OpenAI-compatible providers.",
|
|
473
|
+
"displayName": "Cursor OpenAI BYOK",
|
|
474
|
+
"publisher": "cursor-openai-byok",
|
|
475
|
+
"type": "commonjs",
|
|
476
|
+
"main": "./lib/extension.js",
|
|
477
|
+
"repository": {
|
|
478
|
+
"type": "git",
|
|
479
|
+
"url": "git+https://git.3weijia.com/base/ai/cursor-openai-byok.git"
|
|
480
|
+
},
|
|
481
|
+
"keywords": [
|
|
482
|
+
"cursor",
|
|
483
|
+
"byok",
|
|
484
|
+
"openai",
|
|
485
|
+
"openai-compatible",
|
|
486
|
+
"local-bridge"
|
|
487
|
+
],
|
|
488
|
+
"files": [
|
|
489
|
+
"bin/",
|
|
490
|
+
"lib/",
|
|
491
|
+
"scripts/install.sh",
|
|
492
|
+
"scripts/uninstall.sh",
|
|
493
|
+
"scripts/install.ps1",
|
|
494
|
+
"scripts/uninstall.ps1",
|
|
495
|
+
"config.example.json",
|
|
496
|
+
"README.md",
|
|
497
|
+
"TROUBLESHOOTING.md"
|
|
498
|
+
],
|
|
499
|
+
"publishConfig": {
|
|
500
|
+
"access": "public",
|
|
501
|
+
"registry": "https://registry.npmjs.org/"
|
|
502
|
+
},
|
|
503
|
+
"activationEvents": [
|
|
504
|
+
"*"
|
|
505
|
+
],
|
|
506
|
+
"contributes": {
|
|
507
|
+
"commands": [
|
|
508
|
+
{
|
|
509
|
+
"command": "cursorOpenaiByok.openSettings",
|
|
510
|
+
"title": "Cursor OpenAI BYOK: Open Settings"
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
"command": "cursorOpenaiByok.startServer",
|
|
514
|
+
"title": "Cursor OpenAI BYOK: Start Server"
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
"command": "cursorOpenaiByok.stopServer",
|
|
518
|
+
"title": "Cursor OpenAI BYOK: Stop Server"
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
"command": "cursorOpenaiByok.restartServer",
|
|
522
|
+
"title": "Cursor OpenAI BYOK: Restart Server"
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
"command": "cursorOpenaiByok.showStatus",
|
|
526
|
+
"title": "Cursor OpenAI BYOK: Show Status"
|
|
527
|
+
}
|
|
528
|
+
]
|
|
529
|
+
},
|
|
530
|
+
"bin": {
|
|
531
|
+
"cursor-openai-byok": "bin/cursor-openai-byok.js"
|
|
532
|
+
},
|
|
533
|
+
"scripts": {
|
|
534
|
+
"build:npm": "node scripts/build-npm.js",
|
|
535
|
+
"test": "node --test",
|
|
536
|
+
"doctor": "node bin/cursor-openai-byok.js doctor",
|
|
537
|
+
"prepack": "npm run build:npm",
|
|
538
|
+
"package": "npm run build:npm && node scripts/package.js"
|
|
539
|
+
},
|
|
540
|
+
"engines": {
|
|
541
|
+
"vscode": "^1.99.0",
|
|
542
|
+
"node": ">=20"
|
|
543
|
+
},
|
|
544
|
+
"license": "UNLICENSED"
|
|
545
|
+
};
|
|
546
|
+
},
|
|
547
|
+
"src/logger.js": function(module, exports, require, __filename, __dirname) {
|
|
548
|
+
"use strict";
|
|
549
|
+
|
|
550
|
+
const fs = require("fs");
|
|
551
|
+
const { ensureConfigDir } = require("./config");
|
|
552
|
+
const { logPath } = require("./paths");
|
|
553
|
+
|
|
554
|
+
function redactText(text) {
|
|
555
|
+
return String(text)
|
|
556
|
+
.replace(/sk-[A-Za-z0-9_-]{8,}/g, "sk-***")
|
|
557
|
+
.replace(/Bearer\s+[A-Za-z0-9._-]{8,}/gi, "Bearer ***");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function log(event, meta) {
|
|
561
|
+
ensureConfigDir();
|
|
562
|
+
const suffix = meta === undefined ? "" : " " + redactText(JSON.stringify(meta));
|
|
563
|
+
fs.appendFileSync(logPath(), `[${new Date().toISOString()}] ${event}${suffix}\n`);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function tailLog(lines = 120) {
|
|
567
|
+
if (!fs.existsSync(logPath())) return "";
|
|
568
|
+
const data = fs.readFileSync(logPath(), "utf8");
|
|
569
|
+
return data.split(/\r?\n/).slice(-lines).join("\n");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
module.exports = { log, tailLog, redactText };
|
|
573
|
+
|
|
574
|
+
},
|
|
575
|
+
"src/cursor-agent-adapter/messages.js": function(module, exports, require, __filename, __dirname) {
|
|
576
|
+
"use strict";
|
|
577
|
+
|
|
578
|
+
const fs = require("fs");
|
|
579
|
+
const path = require("path");
|
|
580
|
+
|
|
581
|
+
const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif"]);
|
|
582
|
+
const IMAGE_MIME_BY_EXT = {
|
|
583
|
+
".png": "image/png",
|
|
584
|
+
".jpg": "image/jpeg",
|
|
585
|
+
".jpeg": "image/jpeg",
|
|
586
|
+
".webp": "image/webp",
|
|
587
|
+
".gif": "image/gif",
|
|
588
|
+
};
|
|
589
|
+
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
590
|
+
|
|
591
|
+
function looksLikeNoise(key, value) {
|
|
592
|
+
const k = String(key || "").toLowerCase();
|
|
593
|
+
if (k.includes("id") || k.includes("uuid") || k.includes("path") || k.includes("url")) return true;
|
|
594
|
+
if (k.includes("model") || k.includes("token") || k.includes("hash")) return true;
|
|
595
|
+
if (/^[a-z0-9_-]{24,}$/i.test(value) && !value.includes(" ")) return true;
|
|
596
|
+
return value.length > 120000;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function collectStrings(value, out = [], pathParts = []) {
|
|
600
|
+
if (value == null) return out;
|
|
601
|
+
if (typeof value === "string") {
|
|
602
|
+
const key = pathParts[pathParts.length - 1] || "";
|
|
603
|
+
const text = value.trim();
|
|
604
|
+
if (text && !looksLikeNoise(key, text)) {
|
|
605
|
+
const lower = String(key).toLowerCase();
|
|
606
|
+
let score = 1;
|
|
607
|
+
if (["text", "content", "prompt", "userMessage", "message", "instruction", "instructions", "query"].includes(key)) score += 20;
|
|
608
|
+
if (lower.includes("text") || lower.includes("content") || lower.includes("prompt") || lower.includes("message")) score += 10;
|
|
609
|
+
if (/[\u4e00-\u9fff]/.test(text)) score += 2;
|
|
610
|
+
out.push({ text, score, path: pathParts.join(".") });
|
|
611
|
+
}
|
|
612
|
+
return out;
|
|
613
|
+
}
|
|
614
|
+
if (Array.isArray(value)) {
|
|
615
|
+
value.forEach((item, i) => collectStrings(item, out, pathParts.concat(String(i))));
|
|
616
|
+
return out;
|
|
617
|
+
}
|
|
618
|
+
if (typeof value === "object") {
|
|
619
|
+
for (const [key, child] of Object.entries(value)) collectStrings(child, out, pathParts.concat(key));
|
|
620
|
+
}
|
|
621
|
+
return out;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function extractUserText(payload) {
|
|
625
|
+
const direct =
|
|
626
|
+
payload?.userText ||
|
|
627
|
+
payload?.text ||
|
|
628
|
+
payload?.runRequest?.action?.userMessageAction?.userMessage?.text ||
|
|
629
|
+
payload?.runRequest?.action?.userMessageAction?.prependUserMessages?.at?.(-1)?.text;
|
|
630
|
+
if (typeof direct === "string" && direct.trim()) return direct.trim();
|
|
631
|
+
const candidates = collectStrings(payload)
|
|
632
|
+
.filter((candidate) => candidate.text.length <= 20000)
|
|
633
|
+
.sort((a, b) => a.score - b.score);
|
|
634
|
+
const chosen = candidates[candidates.length - 1];
|
|
635
|
+
return chosen ? chosen.text : "";
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function parseJsonMaybe(value) {
|
|
639
|
+
if (value == null) return undefined;
|
|
640
|
+
if (typeof value === "object") return value;
|
|
641
|
+
if (typeof value !== "string") return undefined;
|
|
642
|
+
try {
|
|
643
|
+
return JSON.parse(value);
|
|
644
|
+
} catch {
|
|
645
|
+
return undefined;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function extractConversationMessages(payload) {
|
|
650
|
+
const messages = [];
|
|
651
|
+
const turns = payload?.runRequest?.conversationState?.turns || payload?.conversationState?.turns || [];
|
|
652
|
+
for (const turn of turns) {
|
|
653
|
+
const parsed = parseJsonMaybe(turn);
|
|
654
|
+
const text = extractUserText(parsed || turn);
|
|
655
|
+
if (text) messages.push({ role: "user", content: text });
|
|
656
|
+
}
|
|
657
|
+
const current = extractUserText(payload);
|
|
658
|
+
const images = extractImages(payload);
|
|
659
|
+
if (current || images.length > 0) messages.push({ role: "user", content: current, images });
|
|
660
|
+
if (messages.length === 0) messages.push({ role: "user", content: "Please reply briefly." });
|
|
661
|
+
return compactMessages(messages);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function compactMessages(messages) {
|
|
665
|
+
const out = [];
|
|
666
|
+
for (const message of messages) {
|
|
667
|
+
if (!message.content && !message.images?.length) continue;
|
|
668
|
+
const prev = out[out.length - 1];
|
|
669
|
+
if (prev && prev.role === message.role && prev.content === message.content) continue;
|
|
670
|
+
const images = Array.isArray(message.images) && message.images.length > 0 ? message.images : undefined;
|
|
671
|
+
out.push({
|
|
672
|
+
role: message.role,
|
|
673
|
+
content: message.content || "",
|
|
674
|
+
...(images ? { images } : {}),
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
return out.slice(-32);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function toResponsesInput(messages) {
|
|
681
|
+
return messages.map((message) => ({
|
|
682
|
+
role: message.role === "assistant" ? "assistant" : "user",
|
|
683
|
+
content: toResponsesContent(message),
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function toChatMessages(messages) {
|
|
688
|
+
return messages.map((message) => ({
|
|
689
|
+
role: message.role === "assistant" ? "assistant" : "user",
|
|
690
|
+
content: toChatContent(message),
|
|
691
|
+
}));
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function toResponsesContent(message) {
|
|
695
|
+
const content = [];
|
|
696
|
+
const text = String(message.content || "");
|
|
697
|
+
if (text || !message.images?.length) {
|
|
698
|
+
content.push({
|
|
699
|
+
type: message.role === "assistant" ? "output_text" : "input_text",
|
|
700
|
+
text,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
if (message.role !== "assistant") {
|
|
704
|
+
for (const image of message.images || []) {
|
|
705
|
+
content.push({ type: "input_image", image_url: image.url });
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return content;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function toChatContent(message) {
|
|
712
|
+
if (message.role === "assistant" || !message.images?.length) return String(message.content || "");
|
|
713
|
+
const content = [];
|
|
714
|
+
const text = String(message.content || "");
|
|
715
|
+
if (text) content.push({ type: "text", text });
|
|
716
|
+
for (const image of message.images) content.push({ type: "image_url", image_url: { url: image.url } });
|
|
717
|
+
return content;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function cursorPayloadToOpenAI(payload, route) {
|
|
721
|
+
const messages = extractConversationMessages(payload);
|
|
722
|
+
const max = route.model.maxOutputTokens;
|
|
723
|
+
if (route.provider.type === "openai-responses") {
|
|
724
|
+
return {
|
|
725
|
+
model: route.model.apiModel,
|
|
726
|
+
stream: true,
|
|
727
|
+
input: toResponsesInput(messages),
|
|
728
|
+
tools: normalizeTools(payload?.tools || payload?.runRequest?.mcpTools),
|
|
729
|
+
tool_choice: payload?.tool_choice || "auto",
|
|
730
|
+
parallel_tool_calls: payload?.parallel_tool_calls !== false,
|
|
731
|
+
max_output_tokens: payload?.max_output_tokens || max,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
model: route.model.apiModel,
|
|
736
|
+
stream: true,
|
|
737
|
+
messages: toChatMessages(messages),
|
|
738
|
+
tools: normalizeTools(payload?.tools || payload?.runRequest?.mcpTools),
|
|
739
|
+
tool_choice: payload?.tool_choice || "auto",
|
|
740
|
+
parallel_tool_calls: payload?.parallel_tool_calls !== false,
|
|
741
|
+
max_tokens: payload?.max_tokens || payload?.max_output_tokens || max,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function normalizeTools(tools) {
|
|
746
|
+
if (!tools) return undefined;
|
|
747
|
+
if (Array.isArray(tools)) return tools;
|
|
748
|
+
const values = Object.values(tools);
|
|
749
|
+
if (values.length === 0) return undefined;
|
|
750
|
+
return values;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function extractImages(payload) {
|
|
754
|
+
const images = [];
|
|
755
|
+
const seen = new Set();
|
|
756
|
+
collectImages(payload, images, seen, []);
|
|
757
|
+
return images.slice(0, 8);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function collectImages(value, out, seen, pathParts) {
|
|
761
|
+
if (value == null || out.length >= 8) return;
|
|
762
|
+
if (typeof value === "string") {
|
|
763
|
+
const image = imageFromString(value, pathParts);
|
|
764
|
+
if (image && !seen.has(image.url)) {
|
|
765
|
+
seen.add(image.url);
|
|
766
|
+
out.push(image);
|
|
767
|
+
}
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (Array.isArray(value)) {
|
|
771
|
+
value.forEach((item, index) => collectImages(item, out, seen, pathParts.concat(String(index))));
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
if (typeof value !== "object") return;
|
|
775
|
+
|
|
776
|
+
const direct = imageFromObject(value, pathParts);
|
|
777
|
+
if (direct && !seen.has(direct.url)) {
|
|
778
|
+
seen.add(direct.url);
|
|
779
|
+
out.push(direct);
|
|
780
|
+
}
|
|
781
|
+
for (const [key, child] of Object.entries(value)) collectImages(child, out, seen, pathParts.concat(key));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function imageFromObject(value, pathParts) {
|
|
785
|
+
const mime = value.mimeType || value.mimetype || value.contentType || value.type;
|
|
786
|
+
if (typeof mime === "string" && mime.startsWith("image/")) {
|
|
787
|
+
const data = value.data || value.base64 || value.bytes || value.content;
|
|
788
|
+
if (typeof data === "string") {
|
|
789
|
+
if (data.startsWith("data:image/")) return { url: data, source: pathParts.join(".") };
|
|
790
|
+
if (looksLikeImageBase64(data)) return { url: `data:${mime};base64,${stripBase64(data)}`, source: pathParts.join(".") };
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
const file = value.path || value.filePath || value.fsPath || value.uri;
|
|
794
|
+
if (typeof file === "string") return imageFromString(file, pathParts);
|
|
795
|
+
const url = value.url || value.imageUrl || value.image_url;
|
|
796
|
+
if (typeof url === "string") return imageFromString(url, pathParts);
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function imageFromString(value, pathParts = []) {
|
|
801
|
+
const text = value.trim();
|
|
802
|
+
if (!text) return null;
|
|
803
|
+
if (/^data:image\/[a-z0-9.+-]+;base64,/i.test(text)) return { url: text, source: pathParts.join(".") };
|
|
804
|
+
if (/^https?:\/\//i.test(text) && looksLikeImagePath(text)) return { url: text, source: pathParts.join(".") };
|
|
805
|
+
if (text.startsWith("file://")) return imageFromFilePath(decodeURIComponent(new URL(text).pathname), pathParts);
|
|
806
|
+
if (looksLikeImagePath(text)) return imageFromFilePath(text, pathParts);
|
|
807
|
+
if (pathParts.some((part) => /image|base64|bytes|data/i.test(part)) && looksLikeImageBase64(text)) {
|
|
808
|
+
const mime = mimeFromImageBase64(text);
|
|
809
|
+
if (mime) return { url: `data:${mime};base64,${stripBase64(text)}`, source: pathParts.join(".") };
|
|
810
|
+
}
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function imageFromFilePath(file, pathParts) {
|
|
815
|
+
try {
|
|
816
|
+
const resolved = path.resolve(String(file).replace(/^~(?=\/|\\)/, process.env.HOME || ""));
|
|
817
|
+
if (!looksLikeImagePath(resolved)) return null;
|
|
818
|
+
const stat = fs.statSync(resolved);
|
|
819
|
+
if (!stat.isFile() || stat.size > MAX_IMAGE_BYTES) return null;
|
|
820
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
821
|
+
const mime = IMAGE_MIME_BY_EXT[ext] || "image/png";
|
|
822
|
+
return { url: `data:${mime};base64,${fs.readFileSync(resolved).toString("base64")}`, source: pathParts.join(".") };
|
|
823
|
+
} catch {
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function looksLikeImagePath(value) {
|
|
829
|
+
try {
|
|
830
|
+
const clean = String(value).split(/[?#]/)[0];
|
|
831
|
+
return IMAGE_EXTENSIONS.has(path.extname(clean).toLowerCase());
|
|
832
|
+
} catch {
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function looksLikeImageBase64(value) {
|
|
838
|
+
const stripped = stripBase64(value);
|
|
839
|
+
if (stripped.length < 32 || stripped.length > MAX_IMAGE_BYTES * 2) return false;
|
|
840
|
+
return !!mimeFromImageBase64(stripped);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function mimeFromImageBase64(value) {
|
|
844
|
+
const stripped = stripBase64(value);
|
|
845
|
+
if (stripped.startsWith("iVBORw0KGgo")) return "image/png";
|
|
846
|
+
if (stripped.startsWith("/9j/")) return "image/jpeg";
|
|
847
|
+
if (stripped.startsWith("UklGR")) return "image/webp";
|
|
848
|
+
if (stripped.startsWith("R0lGOD")) return "image/gif";
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function stripBase64(value) {
|
|
853
|
+
return String(value).replace(/^data:image\/[a-z0-9.+-]+;base64,/i, "").replace(/\s+/g, "");
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
module.exports = {
|
|
857
|
+
extractUserText,
|
|
858
|
+
extractConversationMessages,
|
|
859
|
+
extractImages,
|
|
860
|
+
cursorPayloadToOpenAI,
|
|
861
|
+
toResponsesInput,
|
|
862
|
+
toChatMessages,
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
},
|
|
866
|
+
"src/provider-adapters/openai-compatible.js": function(module, exports, require, __filename, __dirname) {
|
|
867
|
+
"use strict";
|
|
868
|
+
|
|
869
|
+
const { resolveApiKey, resolveModel } = require("../config");
|
|
870
|
+
|
|
871
|
+
function endpointFor(provider, kind) {
|
|
872
|
+
const base = provider.baseUrl.replace(/\/+$/, "");
|
|
873
|
+
if (kind === "responses") return `${base}/responses`;
|
|
874
|
+
if (kind === "chat") return `${base}/chat/completions`;
|
|
875
|
+
throw new Error(`Unknown endpoint kind ${kind}`);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function routeRequestModel(config, body, preferredKind) {
|
|
879
|
+
const route = resolveModel(config, body && body.model);
|
|
880
|
+
const kind = preferredKind || (route.provider.type === "openai-responses" ? "responses" : "chat");
|
|
881
|
+
const mapped = { ...(body || {}), model: route.model.apiModel };
|
|
882
|
+
return { ...route, body: mapped, kind };
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
async function proxyOpenAIRequest(config, kind, body, headers = {}) {
|
|
886
|
+
const route = routeRequestModel(config, body, kind);
|
|
887
|
+
return fetchProviderWithUnsupportedParamRetry(route.provider, route.kind, route.body, headers);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function outboundHeaders(provider, incoming = {}) {
|
|
891
|
+
const headers = {
|
|
892
|
+
"content-type": "application/json",
|
|
893
|
+
authorization: `Bearer ${resolveApiKey(provider)}`,
|
|
894
|
+
};
|
|
895
|
+
for (const [key, value] of Object.entries(incoming || {})) {
|
|
896
|
+
const lower = key.toLowerCase();
|
|
897
|
+
if (lower.startsWith("x-") || lower === "openai-beta") headers[key] = value;
|
|
898
|
+
}
|
|
899
|
+
return headers;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
async function* streamProviderEvents(config, route, openaiBody) {
|
|
903
|
+
const kind = route.provider.type === "openai-responses" ? "responses" : "chat";
|
|
904
|
+
const body = {
|
|
905
|
+
...openaiBody,
|
|
906
|
+
model: route.model.apiModel,
|
|
907
|
+
stream: true,
|
|
908
|
+
};
|
|
909
|
+
const res = await fetchProviderWithUnsupportedParamRetry(route.provider, kind, body);
|
|
910
|
+
if (!res.ok) {
|
|
911
|
+
const text = await res.text();
|
|
912
|
+
throw new Error(`Provider ${res.status}: ${text.slice(0, 1200)}`);
|
|
913
|
+
}
|
|
914
|
+
if (!res.body) throw new Error("Provider response is not streamable");
|
|
915
|
+
for await (const event of parseSse(res.body)) {
|
|
916
|
+
if (event === "[DONE]") break;
|
|
917
|
+
let json;
|
|
918
|
+
try {
|
|
919
|
+
json = JSON.parse(event);
|
|
920
|
+
} catch {
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
yield* normalizeProviderEvent(kind, json);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
async function callProviderOnce(config, route, openaiBody) {
|
|
928
|
+
const kind = route.provider.type === "openai-responses" ? "responses" : "chat";
|
|
929
|
+
const body = { ...openaiBody, model: route.model.apiModel, stream: false };
|
|
930
|
+
const res = await fetchProviderWithUnsupportedParamRetry(route.provider, kind, body);
|
|
931
|
+
const text = await res.text();
|
|
932
|
+
let json;
|
|
933
|
+
try {
|
|
934
|
+
json = JSON.parse(text);
|
|
935
|
+
} catch {
|
|
936
|
+
throw new Error(`Provider returned non-JSON ${res.status}: ${text.slice(0, 500)}`);
|
|
937
|
+
}
|
|
938
|
+
if (!res.ok) throw new Error(`Provider ${res.status}: ${JSON.stringify(json).slice(0, 1200)}`);
|
|
939
|
+
return extractFinalText(kind, json);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async function fetchProviderWithUnsupportedParamRetry(provider, kind, body, headers = {}) {
|
|
943
|
+
const attempted = new Set();
|
|
944
|
+
let current = sanitizeBodyForProvider(provider, body);
|
|
945
|
+
for (;;) {
|
|
946
|
+
const res = await fetch(endpointFor(provider, kind), {
|
|
947
|
+
method: "POST",
|
|
948
|
+
headers: outboundHeaders(provider, headers),
|
|
949
|
+
body: JSON.stringify(current),
|
|
950
|
+
});
|
|
951
|
+
if (res.ok) return res;
|
|
952
|
+
|
|
953
|
+
const text = await res.text();
|
|
954
|
+
const unsupported = parseUnsupportedParameter(text);
|
|
955
|
+
if (!unsupported || attempted.has(unsupported) || !Object.prototype.hasOwnProperty.call(current, unsupported)) {
|
|
956
|
+
return responseFromText(text, res);
|
|
957
|
+
}
|
|
958
|
+
attempted.add(unsupported);
|
|
959
|
+
current = { ...current };
|
|
960
|
+
delete current[unsupported];
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function sanitizeBodyForProvider(provider, body) {
|
|
965
|
+
const out = { ...(body || {}) };
|
|
966
|
+
for (const param of provider.omitParams || provider.unsupportedParams || []) {
|
|
967
|
+
delete out[param];
|
|
968
|
+
}
|
|
969
|
+
return out;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function parseUnsupportedParameter(text) {
|
|
973
|
+
if (!text) return null;
|
|
974
|
+
try {
|
|
975
|
+
const json = JSON.parse(text);
|
|
976
|
+
const message = json?.error?.message || json?.message || "";
|
|
977
|
+
const param = json?.error?.param || json?.param || "";
|
|
978
|
+
if (param && /unsupported parameter/i.test(message)) return param;
|
|
979
|
+
const fromMessage = message.match(/Unsupported parameter:\s*['"]?([A-Za-z0-9_.-]+)['"]?/i);
|
|
980
|
+
if (fromMessage) return fromMessage[1];
|
|
981
|
+
} catch {}
|
|
982
|
+
const match = String(text).match(/Unsupported parameter:\s*['"]?([A-Za-z0-9_.-]+)['"]?/i);
|
|
983
|
+
return match ? match[1] : null;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function responseFromText(text, res) {
|
|
987
|
+
return new Response(text, {
|
|
988
|
+
status: res.status,
|
|
989
|
+
statusText: res.statusText,
|
|
990
|
+
headers: Object.fromEntries(res.headers.entries()),
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
async function* parseSse(body) {
|
|
995
|
+
const decoder = new TextDecoder();
|
|
996
|
+
let buffer = "";
|
|
997
|
+
for await (const chunk of body) {
|
|
998
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
999
|
+
let idx;
|
|
1000
|
+
while ((idx = buffer.indexOf("\n\n")) >= 0) {
|
|
1001
|
+
const raw = buffer.slice(0, idx);
|
|
1002
|
+
buffer = buffer.slice(idx + 2);
|
|
1003
|
+
const data = raw
|
|
1004
|
+
.split(/\r?\n/)
|
|
1005
|
+
.filter((line) => line.startsWith("data:"))
|
|
1006
|
+
.map((line) => line.slice(5).trimStart())
|
|
1007
|
+
.join("\n");
|
|
1008
|
+
if (data) yield data;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
buffer += decoder.decode();
|
|
1012
|
+
const data = buffer
|
|
1013
|
+
.split(/\r?\n/)
|
|
1014
|
+
.filter((line) => line.startsWith("data:"))
|
|
1015
|
+
.map((line) => line.slice(5).trimStart())
|
|
1016
|
+
.join("\n");
|
|
1017
|
+
if (data) yield data;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function* normalizeProviderEvent(kind, event) {
|
|
1021
|
+
if (kind === "chat") {
|
|
1022
|
+
const choice = event.choices && event.choices[0];
|
|
1023
|
+
const delta = choice && choice.delta;
|
|
1024
|
+
if (delta && typeof delta.content === "string" && delta.content) {
|
|
1025
|
+
yield { type: "text_delta", text: delta.content };
|
|
1026
|
+
}
|
|
1027
|
+
for (const toolCall of delta?.tool_calls || []) {
|
|
1028
|
+
yield {
|
|
1029
|
+
type: "tool_call_delta",
|
|
1030
|
+
id: toolCall.id,
|
|
1031
|
+
index: toolCall.index,
|
|
1032
|
+
name: toolCall.function && toolCall.function.name,
|
|
1033
|
+
argumentsDelta: toolCall.function && toolCall.function.arguments,
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
if (choice && choice.finish_reason) yield { type: "finish", reason: choice.finish_reason };
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (event.type === "response.output_text.delta" && typeof event.delta === "string") {
|
|
1041
|
+
yield { type: "text_delta", text: event.delta };
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
if (event.type === "response.function_call_arguments.delta") {
|
|
1045
|
+
yield {
|
|
1046
|
+
type: "tool_call_delta",
|
|
1047
|
+
id: event.item_id,
|
|
1048
|
+
name: event.name,
|
|
1049
|
+
argumentsDelta: event.delta || "",
|
|
1050
|
+
};
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
if (event.type === "response.output_item.done" && event.item && event.item.type === "function_call") {
|
|
1054
|
+
yield {
|
|
1055
|
+
type: "tool_call",
|
|
1056
|
+
id: event.item.call_id || event.item.id,
|
|
1057
|
+
name: event.item.name,
|
|
1058
|
+
arguments: event.item.arguments || "{}",
|
|
1059
|
+
};
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
if (event.type === "response.completed") yield { type: "finish", reason: "completed" };
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function extractFinalText(kind, response) {
|
|
1066
|
+
if (kind === "chat") {
|
|
1067
|
+
return response?.choices?.[0]?.message?.content || "";
|
|
1068
|
+
}
|
|
1069
|
+
if (typeof response.output_text === "string") return response.output_text;
|
|
1070
|
+
const parts = [];
|
|
1071
|
+
for (const item of response.output || []) {
|
|
1072
|
+
for (const content of item.content || []) {
|
|
1073
|
+
if (typeof content.text === "string") parts.push(content.text);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return parts.join("");
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
module.exports = {
|
|
1080
|
+
endpointFor,
|
|
1081
|
+
routeRequestModel,
|
|
1082
|
+
proxyOpenAIRequest,
|
|
1083
|
+
streamProviderEvents,
|
|
1084
|
+
callProviderOnce,
|
|
1085
|
+
parseSse,
|
|
1086
|
+
normalizeProviderEvent,
|
|
1087
|
+
extractFinalText,
|
|
1088
|
+
fetchProviderWithUnsupportedParamRetry,
|
|
1089
|
+
parseUnsupportedParameter,
|
|
1090
|
+
sanitizeBodyForProvider,
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
const __bundleCache = Object.create(null);
|
|
1096
|
+
const __bundleNodeRequire = require;
|
|
1097
|
+
|
|
1098
|
+
function __bundleNormalize(id) {
|
|
1099
|
+
return id.split(__bundlePath.sep).join("/");
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function __bundleResolve(fromId, request) {
|
|
1103
|
+
if (!request.startsWith(".")) return null;
|
|
1104
|
+
const base = __bundlePath.resolve(__bundleRoot, __bundlePath.dirname(fromId), request);
|
|
1105
|
+
for (const candidate of [base, base + ".js", __bundlePath.join(base, "index.js")]) {
|
|
1106
|
+
const id = __bundleNormalize(__bundlePath.relative(__bundleRoot, candidate));
|
|
1107
|
+
if (__bundleModules[id]) return id;
|
|
1108
|
+
}
|
|
1109
|
+
throw new Error("Cannot resolve " + request + " from " + fromId);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function __bundleRequire(id) {
|
|
1113
|
+
if (__bundleCache[id]) return __bundleCache[id].exports;
|
|
1114
|
+
const factory = __bundleModules[id];
|
|
1115
|
+
if (!factory) return __bundleNodeRequire(id);
|
|
1116
|
+
const module = { exports: {} };
|
|
1117
|
+
__bundleCache[id] = module;
|
|
1118
|
+
const filename = __bundlePath.join(__bundleRoot, id);
|
|
1119
|
+
const dirname = __bundlePath.dirname(filename);
|
|
1120
|
+
function localRequire(request) {
|
|
1121
|
+
const localId = __bundleResolve(id, request);
|
|
1122
|
+
return localId ? __bundleRequire(localId) : __bundleNodeRequire(request);
|
|
1123
|
+
}
|
|
1124
|
+
factory(module, module.exports, localRequire, filename, dirname);
|
|
1125
|
+
return module.exports;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
__bundleRequire("src/daemon-entry.js");
|