bridgerapi 1.9.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +293 -246
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
25
|
|
|
26
26
|
// src/server.ts
|
|
27
27
|
var import_http = require("http");
|
|
28
|
-
var
|
|
28
|
+
var import_crypto2 = require("crypto");
|
|
29
29
|
|
|
30
30
|
// src/messages.ts
|
|
31
31
|
function extractText(content) {
|
|
@@ -41,17 +41,24 @@ function messagesToPrompt(messages) {
|
|
|
41
41
|
for (const m of messages) {
|
|
42
42
|
const text = extractText(m.content).trim();
|
|
43
43
|
if (!text) continue;
|
|
44
|
-
|
|
45
|
-
system
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
switch (m.role) {
|
|
45
|
+
case "system":
|
|
46
|
+
system.push(text);
|
|
47
|
+
break;
|
|
48
|
+
case "assistant":
|
|
49
|
+
turns.push(`Assistant: ${text}`);
|
|
50
|
+
break;
|
|
51
|
+
case "tool":
|
|
52
|
+
turns.push(`Tool result: ${text}`);
|
|
53
|
+
break;
|
|
54
|
+
default:
|
|
55
|
+
turns.push(`User: ${text}`);
|
|
56
|
+
break;
|
|
50
57
|
}
|
|
51
58
|
}
|
|
52
59
|
const parts = [];
|
|
53
60
|
if (system.length) parts.push("System instructions:\n" + system.join("\n"));
|
|
54
|
-
if (turns.length) parts.push(turns.join("\n"));
|
|
61
|
+
if (turns.length) parts.push(turns.join("\n\n"));
|
|
55
62
|
return parts.join("\n\n");
|
|
56
63
|
}
|
|
57
64
|
|
|
@@ -63,6 +70,7 @@ var import_path2 = require("path");
|
|
|
63
70
|
|
|
64
71
|
// src/config.ts
|
|
65
72
|
var import_fs = require("fs");
|
|
73
|
+
var import_crypto = require("crypto");
|
|
66
74
|
var import_os = require("os");
|
|
67
75
|
var import_path = require("path");
|
|
68
76
|
var CONFIG_DIR = (0, import_path.join)((0, import_os.homedir)(), ".bridgerapi");
|
|
@@ -80,12 +88,19 @@ function saveConfig(cfg) {
|
|
|
80
88
|
(0, import_fs.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
81
89
|
(0, import_fs.writeFileSync)(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n");
|
|
82
90
|
}
|
|
91
|
+
function getOrCreateApiKey() {
|
|
92
|
+
const cfg = loadConfig();
|
|
93
|
+
if (cfg.apiKey) return cfg.apiKey;
|
|
94
|
+
const key = `sk-bridger-${(0, import_crypto.randomBytes)(24).toString("hex")}`;
|
|
95
|
+
saveConfig({ ...cfg, apiKey: key });
|
|
96
|
+
return key;
|
|
97
|
+
}
|
|
83
98
|
|
|
84
99
|
// src/backends.ts
|
|
85
100
|
var HOME = (0, import_os2.homedir)();
|
|
86
|
-
function which(
|
|
101
|
+
function which(cmd) {
|
|
87
102
|
try {
|
|
88
|
-
return (0, import_child_process.execFileSync)("which", [
|
|
103
|
+
return (0, import_child_process.execFileSync)("which", [cmd], { encoding: "utf8" }).trim();
|
|
89
104
|
} catch {
|
|
90
105
|
return "";
|
|
91
106
|
}
|
|
@@ -150,8 +165,8 @@ async function tryDiscover(bin, strategies) {
|
|
|
150
165
|
}
|
|
151
166
|
return [];
|
|
152
167
|
}
|
|
153
|
-
async function* spawnStream(
|
|
154
|
-
const proc = (0, import_child_process.spawn)(
|
|
168
|
+
async function* spawnStream(cmd, args, stdin) {
|
|
169
|
+
const proc = (0, import_child_process.spawn)(cmd, args, { env: process.env, stdio: ["pipe", "pipe", "pipe"] });
|
|
155
170
|
if (stdin !== void 0) proc.stdin.end(stdin);
|
|
156
171
|
const stderrBufs = [];
|
|
157
172
|
proc.stderr.on("data", (c) => stderrBufs.push(c));
|
|
@@ -169,7 +184,7 @@ async function* spawnStream(cmd2, args, stdin) {
|
|
|
169
184
|
});
|
|
170
185
|
if (!hasOutput && exitCode !== 0) {
|
|
171
186
|
const stderr = Buffer.concat(stderrBufs).toString().trim();
|
|
172
|
-
throw new Error(stderr || `${(0, import_path2.basename)(
|
|
187
|
+
throw new Error(stderr || `${(0, import_path2.basename)(cmd)} exited with code ${exitCode}`);
|
|
173
188
|
}
|
|
174
189
|
}
|
|
175
190
|
var GenericBackend = class {
|
|
@@ -199,11 +214,11 @@ var GenericBackend = class {
|
|
|
199
214
|
}
|
|
200
215
|
return this.def.models ?? [];
|
|
201
216
|
}
|
|
202
|
-
buildArgs(
|
|
203
|
-
return this.def.args.map((a) => a === "{model}" ?
|
|
217
|
+
buildArgs(model) {
|
|
218
|
+
return this.def.args.map((a) => a === "{model}" ? model : a);
|
|
204
219
|
}
|
|
205
|
-
async runBlocking(prompt,
|
|
206
|
-
const args = this.buildArgs(
|
|
220
|
+
async runBlocking(prompt, model) {
|
|
221
|
+
const args = this.buildArgs(model);
|
|
207
222
|
let out;
|
|
208
223
|
try {
|
|
209
224
|
if (this.def.promptMode === "stdin") {
|
|
@@ -216,8 +231,8 @@ var GenericBackend = class {
|
|
|
216
231
|
}
|
|
217
232
|
return [out.trim(), null];
|
|
218
233
|
}
|
|
219
|
-
async *stream(prompt,
|
|
220
|
-
const args = this.buildArgs(
|
|
234
|
+
async *stream(prompt, model) {
|
|
235
|
+
const args = this.buildArgs(model);
|
|
221
236
|
if (this.def.promptMode === "stdin") {
|
|
222
237
|
yield* spawnStream(this.bin, args, prompt);
|
|
223
238
|
} else {
|
|
@@ -248,21 +263,22 @@ var ClaudeBackend = class {
|
|
|
248
263
|
return [
|
|
249
264
|
"claude-opus-4-6",
|
|
250
265
|
"claude-opus-4-6-fast",
|
|
266
|
+
"claude-opus-4-5-20251101",
|
|
251
267
|
"claude-sonnet-4-6",
|
|
252
268
|
"claude-sonnet-4-5-20250929",
|
|
253
269
|
"claude-haiku-4-5-20251001"
|
|
254
270
|
];
|
|
255
271
|
}
|
|
256
|
-
async runBlocking(prompt,
|
|
272
|
+
async runBlocking(prompt, model) {
|
|
257
273
|
let out;
|
|
258
274
|
try {
|
|
259
|
-
out = (0, import_child_process.execFileSync)(this.bin, ["-p", "--output-format", "json", "--model",
|
|
275
|
+
out = (0, import_child_process.execFileSync)(this.bin, ["-p", "--output-format", "json", "--model", model], {
|
|
260
276
|
input: prompt,
|
|
261
277
|
encoding: "utf8",
|
|
262
278
|
timeout: 3e5
|
|
263
279
|
});
|
|
264
280
|
} catch (e) {
|
|
265
|
-
throw new Error(e.stderr?.trim() ||
|
|
281
|
+
throw new Error(e.stderr?.trim() || "claude exited non-zero");
|
|
266
282
|
}
|
|
267
283
|
try {
|
|
268
284
|
const data = JSON.parse(out.trim() || "{}");
|
|
@@ -271,8 +287,8 @@ var ClaudeBackend = class {
|
|
|
271
287
|
return [out.trim(), null];
|
|
272
288
|
}
|
|
273
289
|
}
|
|
274
|
-
async *stream(prompt,
|
|
275
|
-
yield* spawnStream(this.bin, ["-p", "--output-format", "text", "--model",
|
|
290
|
+
async *stream(prompt, model) {
|
|
291
|
+
yield* spawnStream(this.bin, ["-p", "--output-format", "text", "--model", model], prompt);
|
|
276
292
|
}
|
|
277
293
|
};
|
|
278
294
|
var GeminiBackend = class {
|
|
@@ -297,18 +313,16 @@ var GeminiBackend = class {
|
|
|
297
313
|
if (found.length > 0) return found;
|
|
298
314
|
return [
|
|
299
315
|
"gemini-3.1-pro-preview",
|
|
300
|
-
"gemini-3-pro-preview",
|
|
301
316
|
"gemini-3-flash-preview",
|
|
302
317
|
"gemini-2.5-pro",
|
|
303
318
|
"gemini-2.5-flash",
|
|
304
|
-
"gemini-2.0-flash"
|
|
305
|
-
"gemini-1.5-pro"
|
|
319
|
+
"gemini-2.0-flash"
|
|
306
320
|
];
|
|
307
321
|
}
|
|
308
|
-
async runBlocking(prompt,
|
|
322
|
+
async runBlocking(prompt, model) {
|
|
309
323
|
let out;
|
|
310
324
|
try {
|
|
311
|
-
out = (0, import_child_process.execFileSync)(this.bin, ["--output-format", "json", "--model",
|
|
325
|
+
out = (0, import_child_process.execFileSync)(this.bin, ["--output-format", "json", "--model", model, "--approval-mode", "yolo"], {
|
|
312
326
|
input: prompt,
|
|
313
327
|
encoding: "utf8",
|
|
314
328
|
timeout: 3e5,
|
|
@@ -317,8 +331,8 @@ var GeminiBackend = class {
|
|
|
317
331
|
} catch (e) {
|
|
318
332
|
const err = e.stderr?.trim() ?? "";
|
|
319
333
|
if (/auth|login|sign.?in/i.test(err))
|
|
320
|
-
throw new Error(
|
|
321
|
-
throw new Error(err ||
|
|
334
|
+
throw new Error("Gemini not authenticated. Run: gemini auth OR export GEMINI_API_KEY=<key>");
|
|
335
|
+
throw new Error(err || "gemini exited non-zero");
|
|
322
336
|
}
|
|
323
337
|
const raw = out.trim();
|
|
324
338
|
try {
|
|
@@ -328,10 +342,10 @@ var GeminiBackend = class {
|
|
|
328
342
|
return [raw, null];
|
|
329
343
|
}
|
|
330
344
|
}
|
|
331
|
-
async *stream(prompt,
|
|
345
|
+
async *stream(prompt, model) {
|
|
332
346
|
yield* spawnStream(
|
|
333
347
|
this.bin,
|
|
334
|
-
["--output-format", "text", "--model",
|
|
348
|
+
["--output-format", "text", "--model", model, "--approval-mode", "yolo"],
|
|
335
349
|
prompt
|
|
336
350
|
);
|
|
337
351
|
}
|
|
@@ -339,7 +353,7 @@ var GeminiBackend = class {
|
|
|
339
353
|
var CodexBackend = class {
|
|
340
354
|
constructor() {
|
|
341
355
|
this.name = "codex";
|
|
342
|
-
this.prefixes = ["gpt", "o3", "o4"
|
|
356
|
+
this.prefixes = ["gpt", "o3", "o4"];
|
|
343
357
|
}
|
|
344
358
|
get bin() {
|
|
345
359
|
return process.env.CODEX_BIN ?? which("codex") ?? "codex";
|
|
@@ -356,38 +370,35 @@ var CodexBackend = class {
|
|
|
356
370
|
]);
|
|
357
371
|
if (found.length > 0) return found;
|
|
358
372
|
return [
|
|
359
|
-
"gpt-5-codex",
|
|
360
|
-
"gpt-5.1-codex",
|
|
361
|
-
"gpt-5.1-codex-max",
|
|
362
|
-
"gpt-5.1",
|
|
363
373
|
"gpt-5.2",
|
|
364
374
|
"gpt-5.2-codex",
|
|
365
375
|
"gpt-5.3-codex",
|
|
366
|
-
"gpt-5
|
|
376
|
+
"gpt-5.4",
|
|
377
|
+
"gpt-5.4-mini",
|
|
367
378
|
"o4-mini",
|
|
368
379
|
"o3"
|
|
369
380
|
];
|
|
370
381
|
}
|
|
371
|
-
async runBlocking(prompt,
|
|
382
|
+
async runBlocking(prompt, model) {
|
|
372
383
|
let out;
|
|
373
384
|
try {
|
|
374
|
-
out = (0, import_child_process.execFileSync)(this.bin, ["-q", "--model",
|
|
385
|
+
out = (0, import_child_process.execFileSync)(this.bin, ["-q", "--model", model, prompt], {
|
|
375
386
|
encoding: "utf8",
|
|
376
387
|
timeout: 3e5
|
|
377
388
|
});
|
|
378
389
|
} catch (e) {
|
|
379
|
-
throw new Error(e.stderr?.trim() ||
|
|
390
|
+
throw new Error(e.stderr?.trim() || "codex exited non-zero");
|
|
380
391
|
}
|
|
381
392
|
return [out.trim(), null];
|
|
382
393
|
}
|
|
383
|
-
async *stream(prompt,
|
|
384
|
-
yield* spawnStream(this.bin, ["-q", "--model",
|
|
394
|
+
async *stream(prompt, model) {
|
|
395
|
+
yield* spawnStream(this.bin, ["-q", "--model", model, prompt]);
|
|
385
396
|
}
|
|
386
397
|
};
|
|
387
398
|
var CopilotBackend = class {
|
|
388
399
|
constructor() {
|
|
389
400
|
this.name = "copilot";
|
|
390
|
-
this.prefixes = ["copilot"
|
|
401
|
+
this.prefixes = ["copilot"];
|
|
391
402
|
}
|
|
392
403
|
get bin() {
|
|
393
404
|
return process.env.GH_BIN ?? which("gh") ?? "gh";
|
|
@@ -404,53 +415,47 @@ var CopilotBackend = class {
|
|
|
404
415
|
async models() {
|
|
405
416
|
const found = await tryDiscover(this.bin, [
|
|
406
417
|
["copilot", "models"],
|
|
407
|
-
["copilot", "models", "list"]
|
|
408
|
-
["copilot", "--list-models"]
|
|
418
|
+
["copilot", "models", "list"]
|
|
409
419
|
]);
|
|
410
420
|
if (found.length > 0) return found;
|
|
411
421
|
return ["copilot"];
|
|
412
422
|
}
|
|
413
|
-
async runBlocking(prompt,
|
|
423
|
+
async runBlocking(prompt, model) {
|
|
414
424
|
let out;
|
|
415
425
|
try {
|
|
416
|
-
out = (0, import_child_process.execFileSync)(this.bin, ["copilot", "
|
|
426
|
+
out = (0, import_child_process.execFileSync)(this.bin, ["copilot", "explain", prompt], {
|
|
417
427
|
encoding: "utf8",
|
|
418
428
|
timeout: 12e4
|
|
419
429
|
});
|
|
420
430
|
} catch (e) {
|
|
421
|
-
throw new Error(e.stderr?.trim() ||
|
|
431
|
+
throw new Error(e.stderr?.trim() || "gh copilot exited non-zero");
|
|
422
432
|
}
|
|
423
433
|
return [out.trim(), null];
|
|
424
434
|
}
|
|
425
|
-
async *stream(prompt,
|
|
426
|
-
yield* spawnStream(this.bin, ["copilot", "
|
|
435
|
+
async *stream(prompt, model) {
|
|
436
|
+
yield* spawnStream(this.bin, ["copilot", "explain", prompt]);
|
|
427
437
|
}
|
|
428
438
|
};
|
|
429
439
|
var DroidBackend = class _DroidBackend {
|
|
430
440
|
constructor() {
|
|
431
441
|
this.name = "droid";
|
|
432
|
-
this.prefixes = ["droid", "glm", "kimi", "minimax"];
|
|
442
|
+
this.prefixes = ["droid", "factory", "glm", "kimi", "minimax"];
|
|
433
443
|
}
|
|
434
444
|
static {
|
|
435
|
-
// Up-to-date as of March 2026 — source: Factory docs + droid exec --help
|
|
436
445
|
this.KNOWN_MODELS = [
|
|
437
|
-
"gpt-5-codex",
|
|
438
|
-
"gpt-5.1-codex",
|
|
439
|
-
"gpt-5.1-codex-max",
|
|
440
|
-
"gpt-5.1",
|
|
441
|
-
"gpt-5.2",
|
|
442
|
-
"gpt-5.2-codex",
|
|
443
|
-
"gpt-5.3-codex",
|
|
444
|
-
"gpt-5-2025-08-07",
|
|
445
446
|
"claude-opus-4-6",
|
|
446
447
|
"claude-opus-4-6-fast",
|
|
447
|
-
"claude-opus-4-
|
|
448
|
+
"claude-opus-4-5-20251101",
|
|
449
|
+
"claude-sonnet-4-6",
|
|
448
450
|
"claude-sonnet-4-5-20250929",
|
|
449
451
|
"claude-haiku-4-5-20251001",
|
|
452
|
+
"gpt-5.2",
|
|
453
|
+
"gpt-5.2-codex",
|
|
454
|
+
"gpt-5.3-codex",
|
|
455
|
+
"gpt-5.4",
|
|
456
|
+
"gpt-5.4-mini",
|
|
450
457
|
"gemini-3.1-pro-preview",
|
|
451
|
-
"gemini-3-pro-preview",
|
|
452
458
|
"gemini-3-flash-preview",
|
|
453
|
-
"glm-4.6",
|
|
454
459
|
"glm-4.7",
|
|
455
460
|
"glm-5",
|
|
456
461
|
"kimi-k2.5",
|
|
@@ -476,23 +481,23 @@ var DroidBackend = class _DroidBackend {
|
|
|
476
481
|
}
|
|
477
482
|
return _DroidBackend.KNOWN_MODELS;
|
|
478
483
|
}
|
|
479
|
-
async runBlocking(prompt,
|
|
484
|
+
async runBlocking(prompt, model) {
|
|
480
485
|
let out;
|
|
481
486
|
try {
|
|
482
487
|
out = (0, import_child_process.execFileSync)(
|
|
483
488
|
which("droid") || this.bin,
|
|
484
|
-
["exec", "--output-format", "text", "--model",
|
|
489
|
+
["exec", "--output-format", "text", "--model", model],
|
|
485
490
|
{ input: prompt, encoding: "utf8", timeout: 3e5 }
|
|
486
491
|
);
|
|
487
492
|
} catch (e) {
|
|
488
|
-
throw new Error(e.stderr?.trim() ||
|
|
493
|
+
throw new Error(e.stderr?.trim() || "droid exited non-zero");
|
|
489
494
|
}
|
|
490
495
|
return [out.trim(), null];
|
|
491
496
|
}
|
|
492
|
-
async *stream(prompt,
|
|
497
|
+
async *stream(prompt, model) {
|
|
493
498
|
yield* spawnStream(
|
|
494
499
|
which("droid") || this.bin,
|
|
495
|
-
["exec", "--output-format", "text", "--model",
|
|
500
|
+
["exec", "--output-format", "text", "--model", model],
|
|
496
501
|
prompt
|
|
497
502
|
);
|
|
498
503
|
}
|
|
@@ -513,13 +518,13 @@ function loadUserBackends() {
|
|
|
513
518
|
}
|
|
514
519
|
}
|
|
515
520
|
var BACKENDS = [...BUILTIN, ...loadUserBackends()];
|
|
516
|
-
function pickBackend(
|
|
521
|
+
function pickBackend(model) {
|
|
517
522
|
const override = process.env.BRIDGERAPI_BACKEND?.toLowerCase();
|
|
518
523
|
if (override) {
|
|
519
524
|
const forced = BACKENDS.find((b) => b.name === override && b.available());
|
|
520
525
|
if (forced) return forced;
|
|
521
526
|
}
|
|
522
|
-
const m =
|
|
527
|
+
const m = model.toLowerCase();
|
|
523
528
|
for (const b of BACKENDS) {
|
|
524
529
|
if (b.prefixes.some((p) => m.startsWith(p)) && b.available()) return b;
|
|
525
530
|
}
|
|
@@ -532,23 +537,23 @@ function sse(data) {
|
|
|
532
537
|
|
|
533
538
|
`;
|
|
534
539
|
}
|
|
535
|
-
function chunk(id, ts,
|
|
540
|
+
function chunk(id, ts, model, delta, finish) {
|
|
536
541
|
return sse({
|
|
537
542
|
id,
|
|
538
543
|
object: "chat.completion.chunk",
|
|
539
544
|
created: ts,
|
|
540
|
-
model
|
|
545
|
+
model,
|
|
541
546
|
choices: [{ index: 0, delta, finish_reason: finish ?? null }]
|
|
542
547
|
});
|
|
543
548
|
}
|
|
544
|
-
function completion(id, ts,
|
|
549
|
+
function completion(id, ts, model, text, usage) {
|
|
545
550
|
const pt = usage ? (usage.input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.promptTokenCount ?? 0) : 0;
|
|
546
551
|
const ct = usage ? (usage.output_tokens ?? 0) + (usage.candidatesTokenCount ?? 0) : 0;
|
|
547
552
|
return {
|
|
548
553
|
id,
|
|
549
554
|
object: "chat.completion",
|
|
550
555
|
created: ts,
|
|
551
|
-
model
|
|
556
|
+
model,
|
|
552
557
|
choices: [{ index: 0, message: { role: "assistant", content: text }, finish_reason: "stop" }],
|
|
553
558
|
usage: { prompt_tokens: pt, completion_tokens: ct, total_tokens: pt + ct }
|
|
554
559
|
};
|
|
@@ -595,10 +600,10 @@ async function handleModels(res) {
|
|
|
595
600
|
}
|
|
596
601
|
sendJson(res, 200, { object: "list", data: allModels });
|
|
597
602
|
}
|
|
598
|
-
function handleHealth(res,
|
|
603
|
+
function handleHealth(res, port) {
|
|
599
604
|
const backends = {};
|
|
600
605
|
for (const b of BACKENDS) backends[b.name] = b.available();
|
|
601
|
-
sendJson(res, 200, { status: "ok", port
|
|
606
|
+
sendJson(res, 200, { status: "ok", port, backends });
|
|
602
607
|
}
|
|
603
608
|
async function handleChat(req, res) {
|
|
604
609
|
let body;
|
|
@@ -613,68 +618,74 @@ async function handleChat(req, res) {
|
|
|
613
618
|
sendJson(res, 400, { error: { message: "messages required", type: "invalid_request_error" } });
|
|
614
619
|
return;
|
|
615
620
|
}
|
|
616
|
-
const
|
|
621
|
+
const model = body.model ?? "claude-opus-4-6";
|
|
617
622
|
const streaming = Boolean(body.stream);
|
|
618
623
|
const prompt = messagesToPrompt(messages);
|
|
619
|
-
const
|
|
620
|
-
const id = `chatcmpl-${(0,
|
|
624
|
+
const backend = pickBackend(model);
|
|
625
|
+
const id = `chatcmpl-${(0, import_crypto2.randomUUID)().replace(/-/g, "").slice(0, 20)}`;
|
|
621
626
|
const ts = Math.floor(Date.now() / 1e3);
|
|
622
|
-
console.log(` ${
|
|
627
|
+
console.log(` ${backend.name} model=${model} stream=${streaming} turns=${messages.length}`);
|
|
623
628
|
if (streaming) {
|
|
624
629
|
cors(res, 200);
|
|
625
630
|
res.setHeader("Content-Type", "text/event-stream");
|
|
626
631
|
res.setHeader("Cache-Control", "no-cache");
|
|
627
632
|
res.setHeader("X-Accel-Buffering", "no");
|
|
628
633
|
res.flushHeaders();
|
|
629
|
-
res.write(chunk(id, ts,
|
|
634
|
+
res.write(chunk(id, ts, model, { role: "assistant" }));
|
|
630
635
|
try {
|
|
631
|
-
for await (const raw of
|
|
632
|
-
res.write(chunk(id, ts,
|
|
636
|
+
for await (const raw of backend.stream(prompt, model)) {
|
|
637
|
+
res.write(chunk(id, ts, model, { content: raw.toString("utf8") }));
|
|
633
638
|
}
|
|
634
639
|
} catch (err) {
|
|
635
640
|
const msg = err.message ?? String(err);
|
|
636
|
-
console.error(` stream error [${
|
|
637
|
-
res.write(chunk(id, ts,
|
|
641
|
+
console.error(` stream error [${backend.name}]: ${msg}`);
|
|
642
|
+
res.write(chunk(id, ts, model, { content: `
|
|
638
643
|
|
|
639
|
-
|
|
644
|
+
[bridgerapi] ${backend.name} error: ${msg}` }));
|
|
640
645
|
}
|
|
641
|
-
res.write(chunk(id, ts,
|
|
646
|
+
res.write(chunk(id, ts, model, {}, "stop"));
|
|
642
647
|
res.write("data: [DONE]\n\n");
|
|
643
648
|
res.end();
|
|
644
649
|
} else {
|
|
645
650
|
try {
|
|
646
|
-
const [text, usage] = await
|
|
647
|
-
sendJson(res, 200, completion(id, ts,
|
|
651
|
+
const [text, usage] = await backend.runBlocking(prompt, model);
|
|
652
|
+
sendJson(res, 200, completion(id, ts, model, text, usage));
|
|
648
653
|
} catch (err) {
|
|
649
654
|
console.error(` error: ${err.message}`);
|
|
650
655
|
sendJson(res, 500, { error: { message: err.message, type: "server_error" } });
|
|
651
656
|
}
|
|
652
657
|
}
|
|
653
658
|
}
|
|
654
|
-
function createBridgeServer(
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
659
|
+
function createBridgeServer(port) {
|
|
660
|
+
return (0, import_http.createServer)(async (req, res) => {
|
|
661
|
+
try {
|
|
662
|
+
const path = (req.url ?? "/").split("?")[0];
|
|
663
|
+
const method = req.method ?? "GET";
|
|
664
|
+
if (method === "OPTIONS") {
|
|
665
|
+
cors(res, 200);
|
|
666
|
+
res.end();
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (method === "GET" && (path === "/v1/models" || path === "/models")) {
|
|
670
|
+
await handleModels(res);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (method === "GET" && path === "/health") {
|
|
674
|
+
handleHealth(res, port);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (method === "POST" && (path === "/v1/chat/completions" || path === "/chat/completions")) {
|
|
678
|
+
await handleChat(req, res);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
sendJson(res, 404, { error: { message: "not found", type: "not_found_error" } });
|
|
682
|
+
} catch (err) {
|
|
683
|
+
console.error(" unhandled server error:", err.message);
|
|
684
|
+
if (!res.headersSent) {
|
|
685
|
+
sendJson(res, 500, { error: { message: "internal server error", type: "server_error" } });
|
|
686
|
+
}
|
|
674
687
|
}
|
|
675
|
-
sendJson(res, 404, { error: { message: "not found", type: "not_found_error" } });
|
|
676
688
|
});
|
|
677
|
-
return server;
|
|
678
689
|
}
|
|
679
690
|
|
|
680
691
|
// src/service.ts
|
|
@@ -687,13 +698,13 @@ var LABEL = "com.bridgerapi.server";
|
|
|
687
698
|
function plistPath() {
|
|
688
699
|
return (0, import_path3.join)(HOME2, "Library/LaunchAgents", `${LABEL}.plist`);
|
|
689
700
|
}
|
|
690
|
-
function writePlist(
|
|
701
|
+
function writePlist(port, scriptPath, nodePath, backend) {
|
|
691
702
|
const logDir = (0, import_path3.join)(HOME2, ".bridgerapi");
|
|
692
703
|
(0, import_fs3.mkdirSync)(logDir, { recursive: true });
|
|
693
704
|
(0, import_fs3.mkdirSync)((0, import_path3.join)(HOME2, "Library/LaunchAgents"), { recursive: true });
|
|
694
|
-
const backendEntry =
|
|
705
|
+
const backendEntry = backend ? `
|
|
695
706
|
<key>BRIDGERAPI_BACKEND</key>
|
|
696
|
-
<string>${
|
|
707
|
+
<string>${backend}</string>` : "";
|
|
697
708
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
698
709
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
699
710
|
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -712,13 +723,11 @@ function writePlist(port2, scriptPath, nodePath, backend2) {
|
|
|
712
723
|
<key>EnvironmentVariables</key>
|
|
713
724
|
<dict>
|
|
714
725
|
<key>BRIDGERAPI_PORT</key>
|
|
715
|
-
<string>${
|
|
726
|
+
<string>${port}</string>
|
|
716
727
|
<key>PATH</key>
|
|
717
728
|
<string>${HOME2}/.local/bin:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
|
|
718
729
|
<key>HOME</key>
|
|
719
|
-
<string>${HOME2}</string
|
|
720
|
-
<key>CUSTOM_OKBRIDGER_API_KEY</key>
|
|
721
|
-
<string>local</string>${backendEntry}
|
|
730
|
+
<string>${HOME2}</string>${backendEntry}
|
|
722
731
|
</dict>
|
|
723
732
|
|
|
724
733
|
<key>StandardOutPath</key>
|
|
@@ -739,12 +748,12 @@ function unitPath() {
|
|
|
739
748
|
const configHome = process.env.XDG_CONFIG_HOME ?? (0, import_path3.join)(HOME2, ".config");
|
|
740
749
|
return (0, import_path3.join)(configHome, "systemd/user/bridgerapi.service");
|
|
741
750
|
}
|
|
742
|
-
function writeUnit(
|
|
751
|
+
function writeUnit(port, scriptPath, nodePath, backend) {
|
|
743
752
|
const logDir = (0, import_path3.join)(HOME2, ".bridgerapi");
|
|
744
753
|
(0, import_fs3.mkdirSync)(logDir, { recursive: true });
|
|
745
754
|
(0, import_fs3.mkdirSync)((0, import_path3.join)(HOME2, ".config/systemd/user"), { recursive: true });
|
|
746
|
-
const backendLine =
|
|
747
|
-
Environment=BRIDGERAPI_BACKEND=${
|
|
755
|
+
const backendLine = backend ? `
|
|
756
|
+
Environment=BRIDGERAPI_BACKEND=${backend}` : "";
|
|
748
757
|
const unit = `[Unit]
|
|
749
758
|
Description=bridgerapi \u2014 OpenAI-compatible bridge for AI CLIs
|
|
750
759
|
After=network.target
|
|
@@ -752,9 +761,8 @@ After=network.target
|
|
|
752
761
|
[Service]
|
|
753
762
|
Type=simple
|
|
754
763
|
ExecStart=${nodePath} ${scriptPath} start
|
|
755
|
-
Environment=BRIDGERAPI_PORT=${
|
|
764
|
+
Environment=BRIDGERAPI_PORT=${port}
|
|
756
765
|
Environment=HOME=${HOME2}
|
|
757
|
-
Environment=CUSTOM_OKBRIDGER_API_KEY=local
|
|
758
766
|
Environment=PATH=${HOME2}/.local/bin:/usr/local/bin:/usr/bin:/bin${backendLine}
|
|
759
767
|
Restart=always
|
|
760
768
|
StandardOutput=append:${logDir}/server.log
|
|
@@ -765,7 +773,7 @@ WantedBy=default.target
|
|
|
765
773
|
`;
|
|
766
774
|
(0, import_fs3.writeFileSync)(unitPath(), unit);
|
|
767
775
|
}
|
|
768
|
-
function installService(
|
|
776
|
+
function installService(port, backend) {
|
|
769
777
|
const scriptPath = process.argv[1];
|
|
770
778
|
const nodePath = process.execPath;
|
|
771
779
|
const os = (0, import_os3.platform)();
|
|
@@ -774,17 +782,17 @@ function installService(port2, backend2) {
|
|
|
774
782
|
(0, import_child_process2.execSync)(`launchctl unload "${plistPath()}" 2>/dev/null`, { stdio: "ignore" });
|
|
775
783
|
} catch {
|
|
776
784
|
}
|
|
777
|
-
writePlist(
|
|
785
|
+
writePlist(port, scriptPath, nodePath, backend);
|
|
778
786
|
(0, import_child_process2.execSync)(`launchctl load -w "${plistPath()}"`);
|
|
779
|
-
console.log(
|
|
787
|
+
console.log(` LaunchAgent installed -> ${plistPath()}`);
|
|
780
788
|
} else if (os === "linux") {
|
|
781
|
-
writeUnit(
|
|
789
|
+
writeUnit(port, scriptPath, nodePath, backend);
|
|
782
790
|
try {
|
|
783
791
|
(0, import_child_process2.execSync)("systemctl --user daemon-reload");
|
|
784
792
|
} catch {
|
|
785
793
|
}
|
|
786
794
|
(0, import_child_process2.execSync)("systemctl --user enable --now bridgerapi");
|
|
787
|
-
console.log(
|
|
795
|
+
console.log(" systemd user service installed");
|
|
788
796
|
} else {
|
|
789
797
|
throw new Error(`Auto-install not supported on ${os}. Run 'bridgerapi start' manually.`);
|
|
790
798
|
}
|
|
@@ -799,7 +807,7 @@ function uninstallService() {
|
|
|
799
807
|
} catch {
|
|
800
808
|
}
|
|
801
809
|
(0, import_fs3.unlinkSync)(p);
|
|
802
|
-
console.log("
|
|
810
|
+
console.log(" LaunchAgent removed");
|
|
803
811
|
} else {
|
|
804
812
|
console.log(" bridgerapi service is not installed");
|
|
805
813
|
}
|
|
@@ -816,7 +824,7 @@ function uninstallService() {
|
|
|
816
824
|
} catch {
|
|
817
825
|
}
|
|
818
826
|
}
|
|
819
|
-
console.log("
|
|
827
|
+
console.log(" systemd service removed");
|
|
820
828
|
} else {
|
|
821
829
|
throw new Error(`Not supported on ${os}`);
|
|
822
830
|
}
|
|
@@ -860,6 +868,24 @@ function ask(question) {
|
|
|
860
868
|
});
|
|
861
869
|
});
|
|
862
870
|
}
|
|
871
|
+
function printBanner(port, activeBackend) {
|
|
872
|
+
const apiKey = getOrCreateApiKey();
|
|
873
|
+
const available = BACKENDS.filter((b) => b.available());
|
|
874
|
+
const backendLabel = activeBackend ? `${activeBackend} (all requests routed here)` : available.map((b) => b.name).join(", ") + " (auto-routed by model prefix)";
|
|
875
|
+
console.log();
|
|
876
|
+
console.log(" bridgerapi is running");
|
|
877
|
+
console.log();
|
|
878
|
+
console.log(` Base URL : http://127.0.0.1:${port}/v1`);
|
|
879
|
+
console.log(` API Key : ${apiKey}`);
|
|
880
|
+
console.log();
|
|
881
|
+
console.log(` Backend : ${backendLabel}`);
|
|
882
|
+
console.log(` Logs : ${LOG_DIR}/server.log`);
|
|
883
|
+
console.log();
|
|
884
|
+
console.log(" Goose / OpenAI-compatible config:");
|
|
885
|
+
console.log(` OPENAI_API_BASE=http://127.0.0.1:${port}/v1`);
|
|
886
|
+
console.log(` OPENAI_API_KEY=${apiKey}`);
|
|
887
|
+
console.log();
|
|
888
|
+
}
|
|
863
889
|
async function cmdSetup() {
|
|
864
890
|
console.log();
|
|
865
891
|
console.log(" bridgerapi \u2014 OpenAI-compatible API bridge for AI CLI tools");
|
|
@@ -869,8 +895,8 @@ async function cmdSetup() {
|
|
|
869
895
|
console.log(" Backends detected:\n");
|
|
870
896
|
for (const b of BACKENDS) {
|
|
871
897
|
const ok = b.available();
|
|
872
|
-
console.log(` ${ok ? "
|
|
873
|
-
if (!ok) console.log(`
|
|
898
|
+
console.log(` ${ok ? "+" : "-"} ${b.name}`);
|
|
899
|
+
if (!ok) console.log(` -> ${INSTALL_HINTS[b.name] ?? "not installed"}`);
|
|
874
900
|
}
|
|
875
901
|
console.log();
|
|
876
902
|
if (available.length === 0) {
|
|
@@ -887,7 +913,7 @@ async function cmdSetup() {
|
|
|
887
913
|
const defaultIdx = names.indexOf(currentDefault);
|
|
888
914
|
console.log(" Which backend do you want to use as default?\n");
|
|
889
915
|
names.forEach((name, i) => {
|
|
890
|
-
const marker = i === defaultIdx ? "
|
|
916
|
+
const marker = i === defaultIdx ? " <- default" : "";
|
|
891
917
|
console.log(` ${i + 1} ${name}${marker}`);
|
|
892
918
|
});
|
|
893
919
|
console.log();
|
|
@@ -899,8 +925,8 @@ async function cmdSetup() {
|
|
|
899
925
|
}
|
|
900
926
|
const defaultPort = cfg.port ?? DEFAULT_PORT;
|
|
901
927
|
const portAnswer = await ask(` Port [${defaultPort}]: `);
|
|
902
|
-
const
|
|
903
|
-
saveConfig({ backend: chosenBackend, port
|
|
928
|
+
const port = portAnswer ? parseInt(portAnswer) || defaultPort : defaultPort;
|
|
929
|
+
saveConfig({ ...cfg, backend: chosenBackend, port });
|
|
904
930
|
console.log();
|
|
905
931
|
console.log(" How do you want to run bridgerapi?");
|
|
906
932
|
console.log(" 1 Foreground (stops when terminal closes)");
|
|
@@ -909,66 +935,51 @@ async function cmdSetup() {
|
|
|
909
935
|
const choice = await ask(" Choose [1/2]: ");
|
|
910
936
|
console.log();
|
|
911
937
|
if (choice === "2") {
|
|
912
|
-
cmdInstall(
|
|
938
|
+
cmdInstall(port, chosenBackend);
|
|
913
939
|
} else {
|
|
914
|
-
cmdStart(
|
|
940
|
+
cmdStart(port, chosenBackend);
|
|
915
941
|
}
|
|
916
942
|
}
|
|
917
|
-
function cmdStart(
|
|
943
|
+
function cmdStart(port, backend) {
|
|
918
944
|
(0, import_fs4.mkdirSync)(LOG_DIR, { recursive: true });
|
|
919
945
|
const cfg = loadConfig();
|
|
920
|
-
const activeBackend =
|
|
946
|
+
const activeBackend = backend ?? cfg.backend;
|
|
921
947
|
if (activeBackend) process.env.BRIDGERAPI_BACKEND = activeBackend;
|
|
922
948
|
const available = BACKENDS.filter((b) => b.available());
|
|
923
949
|
if (available.length === 0) {
|
|
924
950
|
console.error(" No CLI backends found. Run: bridgerapi to see setup instructions.");
|
|
925
951
|
process.exit(1);
|
|
926
952
|
}
|
|
927
|
-
const server = createBridgeServer(
|
|
928
|
-
server.listen(
|
|
929
|
-
|
|
930
|
-
console.log();
|
|
931
|
-
console.log(` Base URL : http://127.0.0.1:${port2}/v1`);
|
|
932
|
-
console.log(` API Key : local`);
|
|
933
|
-
console.log();
|
|
934
|
-
const backendLabel = activeBackend ? `${activeBackend} (all requests routed here)` : available.map((b) => b.name).join(", ") + " (auto-routed by model prefix)";
|
|
935
|
-
console.log(` Backend : ${backendLabel}`);
|
|
936
|
-
console.log(` Logs : ${LOG_DIR}/server.log`);
|
|
937
|
-
console.log();
|
|
953
|
+
const server = createBridgeServer(port);
|
|
954
|
+
server.listen(port, "127.0.0.1", () => {
|
|
955
|
+
printBanner(port, activeBackend);
|
|
938
956
|
console.log(" Ctrl+C to stop.");
|
|
939
957
|
});
|
|
940
958
|
server.on("error", (err) => {
|
|
941
959
|
if (err.code === "EADDRINUSE") {
|
|
942
|
-
console.error(` Port ${
|
|
960
|
+
console.error(` Port ${port} is already in use. Try: bridgerapi start --port 9000`);
|
|
943
961
|
} else {
|
|
944
962
|
console.error(" Server error:", err.message);
|
|
945
963
|
}
|
|
946
964
|
process.exit(1);
|
|
947
965
|
});
|
|
948
966
|
}
|
|
949
|
-
function cmdInstall(
|
|
967
|
+
function cmdInstall(port, backend) {
|
|
950
968
|
const cfg = loadConfig();
|
|
951
|
-
const activeBackend =
|
|
969
|
+
const activeBackend = backend ?? cfg.backend;
|
|
952
970
|
try {
|
|
953
|
-
installService(
|
|
971
|
+
installService(port, activeBackend);
|
|
954
972
|
console.log();
|
|
955
|
-
console.log(" Waiting for server to start
|
|
973
|
+
console.log(" Waiting for server to start...");
|
|
956
974
|
let attempts = 0;
|
|
957
975
|
const poll = setInterval(async () => {
|
|
958
976
|
attempts++;
|
|
959
977
|
try {
|
|
960
978
|
const http = await import("http");
|
|
961
|
-
http.get(`http://127.0.0.1:${
|
|
979
|
+
http.get(`http://127.0.0.1:${port}/health`, (res) => {
|
|
962
980
|
if (res.statusCode === 200) {
|
|
963
981
|
clearInterval(poll);
|
|
964
|
-
|
|
965
|
-
console.log(` bridgerapi is running`);
|
|
966
|
-
console.log();
|
|
967
|
-
console.log(` Base URL : http://127.0.0.1:${port2}/v1`);
|
|
968
|
-
console.log(` API Key : local`);
|
|
969
|
-
if (activeBackend) console.log(` Backend : ${activeBackend}`);
|
|
970
|
-
console.log();
|
|
971
|
-
console.log(` Logs : tail -f ${LOG_DIR}/server.log`);
|
|
982
|
+
printBanner(port, activeBackend);
|
|
972
983
|
console.log(` Stop : bridgerapi uninstall`);
|
|
973
984
|
process.exit(0);
|
|
974
985
|
}
|
|
@@ -996,20 +1007,48 @@ function cmdUninstall() {
|
|
|
996
1007
|
process.exit(1);
|
|
997
1008
|
}
|
|
998
1009
|
}
|
|
999
|
-
function cmdStatus(
|
|
1010
|
+
function cmdStatus(port) {
|
|
1000
1011
|
const cfg = loadConfig();
|
|
1001
1012
|
const { running, pid } = serviceStatus();
|
|
1002
1013
|
if (running) {
|
|
1014
|
+
const apiKey = getOrCreateApiKey();
|
|
1003
1015
|
console.log(` bridgerapi is running${pid ? ` (pid ${pid})` : ""}`);
|
|
1004
|
-
console.log(` Base URL : http://127.0.0.1:${
|
|
1005
|
-
console.log(` API Key :
|
|
1016
|
+
console.log(` Base URL : http://127.0.0.1:${port}/v1`);
|
|
1017
|
+
console.log(` API Key : ${apiKey}`);
|
|
1006
1018
|
if (cfg.backend) console.log(` Backend : ${cfg.backend}`);
|
|
1007
1019
|
} else {
|
|
1008
1020
|
console.log(" bridgerapi is not running.");
|
|
1009
|
-
console.log(" Run: bridgerapi
|
|
1010
|
-
console.log(" Run: bridgerapi start
|
|
1011
|
-
console.log(" Run: bridgerapi install
|
|
1021
|
+
console.log(" Run: bridgerapi -> setup wizard");
|
|
1022
|
+
console.log(" Run: bridgerapi start -> start in foreground");
|
|
1023
|
+
console.log(" Run: bridgerapi install -> install background service");
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
function cmdKey(args) {
|
|
1027
|
+
if (args[0] === "reset") {
|
|
1028
|
+
const cfg2 = loadConfig();
|
|
1029
|
+
delete cfg2.apiKey;
|
|
1030
|
+
saveConfig(cfg2);
|
|
1031
|
+
const newKey = getOrCreateApiKey();
|
|
1032
|
+
console.log(` API key regenerated: ${newKey}`);
|
|
1033
|
+
return;
|
|
1012
1034
|
}
|
|
1035
|
+
const apiKey = getOrCreateApiKey();
|
|
1036
|
+
const cfg = loadConfig();
|
|
1037
|
+
const port = cfg.port ?? DEFAULT_PORT;
|
|
1038
|
+
console.log();
|
|
1039
|
+
console.log(" bridgerapi API key (OpenAI-compatible)");
|
|
1040
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1041
|
+
console.log();
|
|
1042
|
+
console.log(` ${apiKey}`);
|
|
1043
|
+
console.log();
|
|
1044
|
+
console.log(" Use this as your OPENAI_API_KEY in Goose, Cursor, etc.");
|
|
1045
|
+
console.log();
|
|
1046
|
+
console.log(" Quick setup:");
|
|
1047
|
+
console.log(` export OPENAI_API_BASE=http://127.0.0.1:${port}/v1`);
|
|
1048
|
+
console.log(` export OPENAI_API_KEY=${apiKey}`);
|
|
1049
|
+
console.log();
|
|
1050
|
+
console.log(" Reset: bridgerapi key reset");
|
|
1051
|
+
console.log();
|
|
1013
1052
|
}
|
|
1014
1053
|
function cmdConfig(args) {
|
|
1015
1054
|
const cfg = loadConfig();
|
|
@@ -1030,7 +1069,7 @@ function cmdConfig(args) {
|
|
|
1030
1069
|
process.exit(1);
|
|
1031
1070
|
}
|
|
1032
1071
|
cfg.backend = val;
|
|
1033
|
-
console.log(` backend
|
|
1072
|
+
console.log(` backend -> ${val}`);
|
|
1034
1073
|
} else if (key === "port") {
|
|
1035
1074
|
const p = parseInt(val);
|
|
1036
1075
|
if (isNaN(p) || p < 1 || p > 65535) {
|
|
@@ -1038,7 +1077,7 @@ function cmdConfig(args) {
|
|
|
1038
1077
|
process.exit(1);
|
|
1039
1078
|
}
|
|
1040
1079
|
cfg.port = p;
|
|
1041
|
-
console.log(` port
|
|
1080
|
+
console.log(` port -> ${p}`);
|
|
1042
1081
|
} else {
|
|
1043
1082
|
console.error(` Unknown key: ${key}`);
|
|
1044
1083
|
console.error(` Valid keys: backend, port`);
|
|
@@ -1059,12 +1098,13 @@ function cmdConfig(args) {
|
|
|
1059
1098
|
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1060
1099
|
console.log(` backend : ${cfg.backend ?? "(auto \u2014 routed by model prefix)"}`);
|
|
1061
1100
|
console.log(` port : ${cfg.port ?? `${DEFAULT_PORT} (default)`}`);
|
|
1101
|
+
console.log(` apiKey : ${cfg.apiKey ?? "(not generated yet \u2014 run: bridgerapi key)"}`);
|
|
1062
1102
|
console.log(` file : ${(0, import_path4.join)((0, import_os4.homedir)(), ".bridgerapi/config.json")}`);
|
|
1063
1103
|
console.log();
|
|
1064
1104
|
console.log(" To change:");
|
|
1065
|
-
console.log(
|
|
1066
|
-
console.log(
|
|
1067
|
-
console.log(
|
|
1105
|
+
console.log(" bridgerapi config set backend=droid");
|
|
1106
|
+
console.log(" bridgerapi config set port=9000");
|
|
1107
|
+
console.log(" bridgerapi config reset");
|
|
1068
1108
|
console.log();
|
|
1069
1109
|
}
|
|
1070
1110
|
async function cmdBackendAdd() {
|
|
@@ -1088,8 +1128,8 @@ async function cmdBackendAdd() {
|
|
|
1088
1128
|
const modeChoice = await ask(" Choose [1/2]: ");
|
|
1089
1129
|
const promptMode = modeChoice === "2" ? "arg" : "stdin";
|
|
1090
1130
|
console.log();
|
|
1091
|
-
console.log(
|
|
1092
|
-
console.log(
|
|
1131
|
+
console.log(" Command arguments template. Use {model} as placeholder for the model name.");
|
|
1132
|
+
console.log(" Example: exec --output-format text --model {model}");
|
|
1093
1133
|
const argsRaw = await ask(" Args: ");
|
|
1094
1134
|
const args = argsRaw.trim().split(/\s+/).filter(Boolean);
|
|
1095
1135
|
const modelsCmdRaw = await ask(" Args to list models (leave blank to skip): ");
|
|
@@ -1104,11 +1144,8 @@ async function cmdBackendAdd() {
|
|
|
1104
1144
|
else existing.push(def);
|
|
1105
1145
|
saveConfig({ ...cfg, customBackends: existing });
|
|
1106
1146
|
console.log();
|
|
1107
|
-
console.log(`
|
|
1108
|
-
console.log(
|
|
1109
|
-
console.log();
|
|
1110
|
-
console.log(" Example JSON entry in ~/.bridgerapi/config.json:");
|
|
1111
|
-
console.log(` ${JSON.stringify(def, null, 2).split("\n").join("\n ")}`);
|
|
1147
|
+
console.log(` ${name} backend saved.`);
|
|
1148
|
+
console.log(" Restart bridgerapi for it to take effect.");
|
|
1112
1149
|
console.log();
|
|
1113
1150
|
}
|
|
1114
1151
|
async function cmdBackendList() {
|
|
@@ -1117,34 +1154,34 @@ async function cmdBackendList() {
|
|
|
1117
1154
|
for (const b of BACKENDS) {
|
|
1118
1155
|
if (!b.available()) {
|
|
1119
1156
|
const hint = INSTALL_HINTS[b.name] ?? "not installed";
|
|
1120
|
-
console.log(`
|
|
1121
|
-
console.log(`
|
|
1157
|
+
console.log(` - ${b.name}`);
|
|
1158
|
+
console.log(` -> ${hint}
|
|
1122
1159
|
`);
|
|
1123
1160
|
continue;
|
|
1124
1161
|
}
|
|
1125
|
-
process.stdout.write(`
|
|
1162
|
+
process.stdout.write(` + ${b.name} (discovering models...)\r`);
|
|
1126
1163
|
const modelList = await b.models();
|
|
1127
1164
|
const preview = modelList.slice(0, 6).join(" ");
|
|
1128
1165
|
const extra = modelList.length > 6 ? ` +${modelList.length - 6} more` : "";
|
|
1129
|
-
console.log(`
|
|
1166
|
+
console.log(` + ${b.name} `);
|
|
1130
1167
|
console.log(` ${preview}${extra}
|
|
1131
1168
|
`);
|
|
1132
1169
|
}
|
|
1133
1170
|
}
|
|
1134
|
-
async function cmdChat(
|
|
1171
|
+
async function cmdChat(model, backendFlag) {
|
|
1135
1172
|
const cfg = loadConfig();
|
|
1136
|
-
const activeBackend = backendFlag ?? (
|
|
1173
|
+
const activeBackend = backendFlag ?? (model && BACKENDS.find((b) => b.name === model?.toLowerCase())?.name) ?? cfg.backend;
|
|
1137
1174
|
if (activeBackend) process.env.BRIDGERAPI_BACKEND = activeBackend;
|
|
1138
|
-
const resolvedModel =
|
|
1175
|
+
const resolvedModel = model && BACKENDS.find((b) => b.name === model.toLowerCase()) ? void 0 : model;
|
|
1139
1176
|
const available = BACKENDS.filter((b) => b.available());
|
|
1140
1177
|
if (available.length === 0) {
|
|
1141
1178
|
console.error(" No backends found. Run: bridgerapi to see setup instructions.");
|
|
1142
1179
|
process.exit(1);
|
|
1143
1180
|
}
|
|
1144
1181
|
const fallbackModel = `${activeBackend ?? available[0].name}-default`;
|
|
1145
|
-
const
|
|
1182
|
+
const backend = pickBackend(resolvedModel ?? fallbackModel);
|
|
1146
1183
|
console.log();
|
|
1147
|
-
console.log(` bridgerapi chat \u2014 ${
|
|
1184
|
+
console.log(` bridgerapi chat \u2014 ${backend.name}`);
|
|
1148
1185
|
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1149
1186
|
console.log(" Type a message and press Enter. Ctrl+C to exit.");
|
|
1150
1187
|
console.log();
|
|
@@ -1165,8 +1202,8 @@ async function cmdChat(model2, backendFlag) {
|
|
|
1165
1202
|
process.stdout.write("\n");
|
|
1166
1203
|
let reply = "";
|
|
1167
1204
|
try {
|
|
1168
|
-
process.stdout.write(`${
|
|
1169
|
-
for await (const chunk2 of
|
|
1205
|
+
process.stdout.write(`${backend.name}: `);
|
|
1206
|
+
for await (const chunk2 of backend.stream(messagesToPrompt(history), resolvedModel ?? fallbackModel)) {
|
|
1170
1207
|
const piece = chunk2.toString("utf8");
|
|
1171
1208
|
process.stdout.write(piece);
|
|
1172
1209
|
reply += piece;
|
|
@@ -1188,79 +1225,89 @@ function showHelp() {
|
|
|
1188
1225
|
|
|
1189
1226
|
Usage:
|
|
1190
1227
|
bridgerapi Interactive setup wizard
|
|
1191
|
-
bridgerapi chat [--model m] Chat in the terminal (routes by model prefix)
|
|
1192
|
-
bridgerapi chat --backend <name> Chat using a specific backend
|
|
1193
1228
|
bridgerapi start [--port n] Start API server in the foreground
|
|
1194
|
-
bridgerapi start --backend <name> Start forcing a specific backend
|
|
1229
|
+
bridgerapi start --backend <name> Start forcing a specific backend
|
|
1230
|
+
bridgerapi chat [--model m] Chat in the terminal
|
|
1231
|
+
bridgerapi chat --backend <name> Chat using a specific backend
|
|
1232
|
+
bridgerapi key Show your OpenAI-compatible API key
|
|
1233
|
+
bridgerapi key reset Regenerate the API key
|
|
1195
1234
|
bridgerapi install [--port n] Install as a background service
|
|
1196
1235
|
bridgerapi uninstall Remove background service
|
|
1197
1236
|
bridgerapi status Show service status
|
|
1198
1237
|
bridgerapi config Show saved configuration
|
|
1199
|
-
bridgerapi config set backend=<b> Set default backend
|
|
1238
|
+
bridgerapi config set backend=<b> Set default backend
|
|
1200
1239
|
bridgerapi config set port=<n> Set default port
|
|
1201
1240
|
bridgerapi config reset Clear saved configuration
|
|
1202
1241
|
bridgerapi backend List all backends (built-in + custom)
|
|
1203
1242
|
bridgerapi backend add Add a custom CLI backend interactively
|
|
1204
1243
|
|
|
1205
1244
|
Built-in backends: claude, gemini, codex, copilot, droid
|
|
1206
|
-
Custom backends: add any AI CLI via "bridgerapi backend add"
|
|
1207
1245
|
`.trim());
|
|
1208
1246
|
}
|
|
1209
1247
|
function parseArgs() {
|
|
1210
1248
|
const cfg = loadConfig();
|
|
1211
1249
|
const args = process.argv.slice(2);
|
|
1212
|
-
const
|
|
1213
|
-
let
|
|
1214
|
-
let
|
|
1215
|
-
let
|
|
1216
|
-
const
|
|
1250
|
+
const cmd = args[0] ?? "";
|
|
1251
|
+
let port = cfg.port ?? DEFAULT_PORT;
|
|
1252
|
+
let model;
|
|
1253
|
+
let backend;
|
|
1254
|
+
const rest = [];
|
|
1217
1255
|
for (let i = 1; i < args.length; i++) {
|
|
1218
1256
|
if ((args[i] === "--port" || args[i] === "-p") && args[i + 1]) {
|
|
1219
|
-
|
|
1257
|
+
port = parseInt(args[++i]);
|
|
1220
1258
|
} else if ((args[i] === "--model" || args[i] === "-m") && args[i + 1]) {
|
|
1221
|
-
|
|
1259
|
+
model = args[++i];
|
|
1222
1260
|
} else if ((args[i] === "--backend" || args[i] === "-b") && args[i + 1]) {
|
|
1223
|
-
|
|
1261
|
+
backend = args[++i];
|
|
1224
1262
|
} else {
|
|
1225
|
-
|
|
1263
|
+
rest.push(args[i]);
|
|
1226
1264
|
}
|
|
1227
1265
|
}
|
|
1228
|
-
return { cmd
|
|
1266
|
+
return { cmd, port, model, backend, rest };
|
|
1229
1267
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1268
|
+
async function main() {
|
|
1269
|
+
const { cmd, port, model, backend, rest } = parseArgs();
|
|
1270
|
+
switch (cmd) {
|
|
1271
|
+
case "":
|
|
1272
|
+
case "setup":
|
|
1273
|
+
await cmdSetup();
|
|
1274
|
+
break;
|
|
1275
|
+
case "chat":
|
|
1276
|
+
await cmdChat(model, backend);
|
|
1277
|
+
break;
|
|
1278
|
+
case "start":
|
|
1279
|
+
cmdStart(port, backend);
|
|
1280
|
+
break;
|
|
1281
|
+
case "install":
|
|
1282
|
+
cmdInstall(port, backend);
|
|
1283
|
+
break;
|
|
1284
|
+
case "uninstall":
|
|
1285
|
+
cmdUninstall();
|
|
1286
|
+
break;
|
|
1287
|
+
case "status":
|
|
1288
|
+
cmdStatus(port);
|
|
1289
|
+
break;
|
|
1290
|
+
case "key":
|
|
1291
|
+
cmdKey(rest);
|
|
1292
|
+
break;
|
|
1293
|
+
case "config":
|
|
1294
|
+
cmdConfig(rest);
|
|
1295
|
+
break;
|
|
1296
|
+
case "backend":
|
|
1297
|
+
if (rest[0] === "add") await cmdBackendAdd();
|
|
1298
|
+
else await cmdBackendList();
|
|
1299
|
+
break;
|
|
1300
|
+
case "help":
|
|
1301
|
+
case "--help":
|
|
1302
|
+
case "-h":
|
|
1303
|
+
showHelp();
|
|
1304
|
+
break;
|
|
1305
|
+
default:
|
|
1306
|
+
showHelp();
|
|
1307
|
+
process.exit(1);
|
|
1308
|
+
}
|
|
1266
1309
|
}
|
|
1310
|
+
main().catch((err) => {
|
|
1311
|
+
console.error(" Fatal:", err.message);
|
|
1312
|
+
process.exit(1);
|
|
1313
|
+
});
|