bridgerapi 1.9.2 → 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 +268 -228
- 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,46 +618,46 @@ 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
|
-
|
|
659
|
+
function createBridgeServer(port) {
|
|
660
|
+
return (0, import_http.createServer)(async (req, res) => {
|
|
656
661
|
try {
|
|
657
662
|
const path = (req.url ?? "/").split("?")[0];
|
|
658
663
|
const method = req.method ?? "GET";
|
|
@@ -666,7 +671,7 @@ function createBridgeServer(port2) {
|
|
|
666
671
|
return;
|
|
667
672
|
}
|
|
668
673
|
if (method === "GET" && path === "/health") {
|
|
669
|
-
handleHealth(res,
|
|
674
|
+
handleHealth(res, port);
|
|
670
675
|
return;
|
|
671
676
|
}
|
|
672
677
|
if (method === "POST" && (path === "/v1/chat/completions" || path === "/chat/completions")) {
|
|
@@ -681,7 +686,6 @@ function createBridgeServer(port2) {
|
|
|
681
686
|
}
|
|
682
687
|
}
|
|
683
688
|
});
|
|
684
|
-
return server;
|
|
685
689
|
}
|
|
686
690
|
|
|
687
691
|
// src/service.ts
|
|
@@ -694,13 +698,13 @@ var LABEL = "com.bridgerapi.server";
|
|
|
694
698
|
function plistPath() {
|
|
695
699
|
return (0, import_path3.join)(HOME2, "Library/LaunchAgents", `${LABEL}.plist`);
|
|
696
700
|
}
|
|
697
|
-
function writePlist(
|
|
701
|
+
function writePlist(port, scriptPath, nodePath, backend) {
|
|
698
702
|
const logDir = (0, import_path3.join)(HOME2, ".bridgerapi");
|
|
699
703
|
(0, import_fs3.mkdirSync)(logDir, { recursive: true });
|
|
700
704
|
(0, import_fs3.mkdirSync)((0, import_path3.join)(HOME2, "Library/LaunchAgents"), { recursive: true });
|
|
701
|
-
const backendEntry =
|
|
705
|
+
const backendEntry = backend ? `
|
|
702
706
|
<key>BRIDGERAPI_BACKEND</key>
|
|
703
|
-
<string>${
|
|
707
|
+
<string>${backend}</string>` : "";
|
|
704
708
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
705
709
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
706
710
|
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -719,13 +723,11 @@ function writePlist(port2, scriptPath, nodePath, backend2) {
|
|
|
719
723
|
<key>EnvironmentVariables</key>
|
|
720
724
|
<dict>
|
|
721
725
|
<key>BRIDGERAPI_PORT</key>
|
|
722
|
-
<string>${
|
|
726
|
+
<string>${port}</string>
|
|
723
727
|
<key>PATH</key>
|
|
724
728
|
<string>${HOME2}/.local/bin:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
|
|
725
729
|
<key>HOME</key>
|
|
726
|
-
<string>${HOME2}</string
|
|
727
|
-
<key>CUSTOM_OKBRIDGER_API_KEY</key>
|
|
728
|
-
<string>local</string>${backendEntry}
|
|
730
|
+
<string>${HOME2}</string>${backendEntry}
|
|
729
731
|
</dict>
|
|
730
732
|
|
|
731
733
|
<key>StandardOutPath</key>
|
|
@@ -746,12 +748,12 @@ function unitPath() {
|
|
|
746
748
|
const configHome = process.env.XDG_CONFIG_HOME ?? (0, import_path3.join)(HOME2, ".config");
|
|
747
749
|
return (0, import_path3.join)(configHome, "systemd/user/bridgerapi.service");
|
|
748
750
|
}
|
|
749
|
-
function writeUnit(
|
|
751
|
+
function writeUnit(port, scriptPath, nodePath, backend) {
|
|
750
752
|
const logDir = (0, import_path3.join)(HOME2, ".bridgerapi");
|
|
751
753
|
(0, import_fs3.mkdirSync)(logDir, { recursive: true });
|
|
752
754
|
(0, import_fs3.mkdirSync)((0, import_path3.join)(HOME2, ".config/systemd/user"), { recursive: true });
|
|
753
|
-
const backendLine =
|
|
754
|
-
Environment=BRIDGERAPI_BACKEND=${
|
|
755
|
+
const backendLine = backend ? `
|
|
756
|
+
Environment=BRIDGERAPI_BACKEND=${backend}` : "";
|
|
755
757
|
const unit = `[Unit]
|
|
756
758
|
Description=bridgerapi \u2014 OpenAI-compatible bridge for AI CLIs
|
|
757
759
|
After=network.target
|
|
@@ -759,9 +761,8 @@ After=network.target
|
|
|
759
761
|
[Service]
|
|
760
762
|
Type=simple
|
|
761
763
|
ExecStart=${nodePath} ${scriptPath} start
|
|
762
|
-
Environment=BRIDGERAPI_PORT=${
|
|
764
|
+
Environment=BRIDGERAPI_PORT=${port}
|
|
763
765
|
Environment=HOME=${HOME2}
|
|
764
|
-
Environment=CUSTOM_OKBRIDGER_API_KEY=local
|
|
765
766
|
Environment=PATH=${HOME2}/.local/bin:/usr/local/bin:/usr/bin:/bin${backendLine}
|
|
766
767
|
Restart=always
|
|
767
768
|
StandardOutput=append:${logDir}/server.log
|
|
@@ -772,7 +773,7 @@ WantedBy=default.target
|
|
|
772
773
|
`;
|
|
773
774
|
(0, import_fs3.writeFileSync)(unitPath(), unit);
|
|
774
775
|
}
|
|
775
|
-
function installService(
|
|
776
|
+
function installService(port, backend) {
|
|
776
777
|
const scriptPath = process.argv[1];
|
|
777
778
|
const nodePath = process.execPath;
|
|
778
779
|
const os = (0, import_os3.platform)();
|
|
@@ -781,17 +782,17 @@ function installService(port2, backend2) {
|
|
|
781
782
|
(0, import_child_process2.execSync)(`launchctl unload "${plistPath()}" 2>/dev/null`, { stdio: "ignore" });
|
|
782
783
|
} catch {
|
|
783
784
|
}
|
|
784
|
-
writePlist(
|
|
785
|
+
writePlist(port, scriptPath, nodePath, backend);
|
|
785
786
|
(0, import_child_process2.execSync)(`launchctl load -w "${plistPath()}"`);
|
|
786
|
-
console.log(
|
|
787
|
+
console.log(` LaunchAgent installed -> ${plistPath()}`);
|
|
787
788
|
} else if (os === "linux") {
|
|
788
|
-
writeUnit(
|
|
789
|
+
writeUnit(port, scriptPath, nodePath, backend);
|
|
789
790
|
try {
|
|
790
791
|
(0, import_child_process2.execSync)("systemctl --user daemon-reload");
|
|
791
792
|
} catch {
|
|
792
793
|
}
|
|
793
794
|
(0, import_child_process2.execSync)("systemctl --user enable --now bridgerapi");
|
|
794
|
-
console.log(
|
|
795
|
+
console.log(" systemd user service installed");
|
|
795
796
|
} else {
|
|
796
797
|
throw new Error(`Auto-install not supported on ${os}. Run 'bridgerapi start' manually.`);
|
|
797
798
|
}
|
|
@@ -806,7 +807,7 @@ function uninstallService() {
|
|
|
806
807
|
} catch {
|
|
807
808
|
}
|
|
808
809
|
(0, import_fs3.unlinkSync)(p);
|
|
809
|
-
console.log("
|
|
810
|
+
console.log(" LaunchAgent removed");
|
|
810
811
|
} else {
|
|
811
812
|
console.log(" bridgerapi service is not installed");
|
|
812
813
|
}
|
|
@@ -823,7 +824,7 @@ function uninstallService() {
|
|
|
823
824
|
} catch {
|
|
824
825
|
}
|
|
825
826
|
}
|
|
826
|
-
console.log("
|
|
827
|
+
console.log(" systemd service removed");
|
|
827
828
|
} else {
|
|
828
829
|
throw new Error(`Not supported on ${os}`);
|
|
829
830
|
}
|
|
@@ -867,6 +868,24 @@ function ask(question) {
|
|
|
867
868
|
});
|
|
868
869
|
});
|
|
869
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
|
+
}
|
|
870
889
|
async function cmdSetup() {
|
|
871
890
|
console.log();
|
|
872
891
|
console.log(" bridgerapi \u2014 OpenAI-compatible API bridge for AI CLI tools");
|
|
@@ -876,8 +895,8 @@ async function cmdSetup() {
|
|
|
876
895
|
console.log(" Backends detected:\n");
|
|
877
896
|
for (const b of BACKENDS) {
|
|
878
897
|
const ok = b.available();
|
|
879
|
-
console.log(` ${ok ? "
|
|
880
|
-
if (!ok) console.log(`
|
|
898
|
+
console.log(` ${ok ? "+" : "-"} ${b.name}`);
|
|
899
|
+
if (!ok) console.log(` -> ${INSTALL_HINTS[b.name] ?? "not installed"}`);
|
|
881
900
|
}
|
|
882
901
|
console.log();
|
|
883
902
|
if (available.length === 0) {
|
|
@@ -894,7 +913,7 @@ async function cmdSetup() {
|
|
|
894
913
|
const defaultIdx = names.indexOf(currentDefault);
|
|
895
914
|
console.log(" Which backend do you want to use as default?\n");
|
|
896
915
|
names.forEach((name, i) => {
|
|
897
|
-
const marker = i === defaultIdx ? "
|
|
916
|
+
const marker = i === defaultIdx ? " <- default" : "";
|
|
898
917
|
console.log(` ${i + 1} ${name}${marker}`);
|
|
899
918
|
});
|
|
900
919
|
console.log();
|
|
@@ -906,8 +925,8 @@ async function cmdSetup() {
|
|
|
906
925
|
}
|
|
907
926
|
const defaultPort = cfg.port ?? DEFAULT_PORT;
|
|
908
927
|
const portAnswer = await ask(` Port [${defaultPort}]: `);
|
|
909
|
-
const
|
|
910
|
-
saveConfig({ backend: chosenBackend, port
|
|
928
|
+
const port = portAnswer ? parseInt(portAnswer) || defaultPort : defaultPort;
|
|
929
|
+
saveConfig({ ...cfg, backend: chosenBackend, port });
|
|
911
930
|
console.log();
|
|
912
931
|
console.log(" How do you want to run bridgerapi?");
|
|
913
932
|
console.log(" 1 Foreground (stops when terminal closes)");
|
|
@@ -916,66 +935,51 @@ async function cmdSetup() {
|
|
|
916
935
|
const choice = await ask(" Choose [1/2]: ");
|
|
917
936
|
console.log();
|
|
918
937
|
if (choice === "2") {
|
|
919
|
-
cmdInstall(
|
|
938
|
+
cmdInstall(port, chosenBackend);
|
|
920
939
|
} else {
|
|
921
|
-
cmdStart(
|
|
940
|
+
cmdStart(port, chosenBackend);
|
|
922
941
|
}
|
|
923
942
|
}
|
|
924
|
-
function cmdStart(
|
|
943
|
+
function cmdStart(port, backend) {
|
|
925
944
|
(0, import_fs4.mkdirSync)(LOG_DIR, { recursive: true });
|
|
926
945
|
const cfg = loadConfig();
|
|
927
|
-
const activeBackend =
|
|
946
|
+
const activeBackend = backend ?? cfg.backend;
|
|
928
947
|
if (activeBackend) process.env.BRIDGERAPI_BACKEND = activeBackend;
|
|
929
948
|
const available = BACKENDS.filter((b) => b.available());
|
|
930
949
|
if (available.length === 0) {
|
|
931
950
|
console.error(" No CLI backends found. Run: bridgerapi to see setup instructions.");
|
|
932
951
|
process.exit(1);
|
|
933
952
|
}
|
|
934
|
-
const server = createBridgeServer(
|
|
935
|
-
server.listen(
|
|
936
|
-
|
|
937
|
-
console.log();
|
|
938
|
-
console.log(` Base URL : http://127.0.0.1:${port2}/v1`);
|
|
939
|
-
console.log(` API Key : local`);
|
|
940
|
-
console.log();
|
|
941
|
-
const backendLabel = activeBackend ? `${activeBackend} (all requests routed here)` : available.map((b) => b.name).join(", ") + " (auto-routed by model prefix)";
|
|
942
|
-
console.log(` Backend : ${backendLabel}`);
|
|
943
|
-
console.log(` Logs : ${LOG_DIR}/server.log`);
|
|
944
|
-
console.log();
|
|
953
|
+
const server = createBridgeServer(port);
|
|
954
|
+
server.listen(port, "127.0.0.1", () => {
|
|
955
|
+
printBanner(port, activeBackend);
|
|
945
956
|
console.log(" Ctrl+C to stop.");
|
|
946
957
|
});
|
|
947
958
|
server.on("error", (err) => {
|
|
948
959
|
if (err.code === "EADDRINUSE") {
|
|
949
|
-
console.error(` Port ${
|
|
960
|
+
console.error(` Port ${port} is already in use. Try: bridgerapi start --port 9000`);
|
|
950
961
|
} else {
|
|
951
962
|
console.error(" Server error:", err.message);
|
|
952
963
|
}
|
|
953
964
|
process.exit(1);
|
|
954
965
|
});
|
|
955
966
|
}
|
|
956
|
-
function cmdInstall(
|
|
967
|
+
function cmdInstall(port, backend) {
|
|
957
968
|
const cfg = loadConfig();
|
|
958
|
-
const activeBackend =
|
|
969
|
+
const activeBackend = backend ?? cfg.backend;
|
|
959
970
|
try {
|
|
960
|
-
installService(
|
|
971
|
+
installService(port, activeBackend);
|
|
961
972
|
console.log();
|
|
962
|
-
console.log(" Waiting for server to start
|
|
973
|
+
console.log(" Waiting for server to start...");
|
|
963
974
|
let attempts = 0;
|
|
964
975
|
const poll = setInterval(async () => {
|
|
965
976
|
attempts++;
|
|
966
977
|
try {
|
|
967
978
|
const http = await import("http");
|
|
968
|
-
http.get(`http://127.0.0.1:${
|
|
979
|
+
http.get(`http://127.0.0.1:${port}/health`, (res) => {
|
|
969
980
|
if (res.statusCode === 200) {
|
|
970
981
|
clearInterval(poll);
|
|
971
|
-
|
|
972
|
-
console.log(` bridgerapi is running`);
|
|
973
|
-
console.log();
|
|
974
|
-
console.log(` Base URL : http://127.0.0.1:${port2}/v1`);
|
|
975
|
-
console.log(` API Key : local`);
|
|
976
|
-
if (activeBackend) console.log(` Backend : ${activeBackend}`);
|
|
977
|
-
console.log();
|
|
978
|
-
console.log(` Logs : tail -f ${LOG_DIR}/server.log`);
|
|
982
|
+
printBanner(port, activeBackend);
|
|
979
983
|
console.log(` Stop : bridgerapi uninstall`);
|
|
980
984
|
process.exit(0);
|
|
981
985
|
}
|
|
@@ -1003,21 +1007,49 @@ function cmdUninstall() {
|
|
|
1003
1007
|
process.exit(1);
|
|
1004
1008
|
}
|
|
1005
1009
|
}
|
|
1006
|
-
function cmdStatus(
|
|
1010
|
+
function cmdStatus(port) {
|
|
1007
1011
|
const cfg = loadConfig();
|
|
1008
1012
|
const { running, pid } = serviceStatus();
|
|
1009
1013
|
if (running) {
|
|
1014
|
+
const apiKey = getOrCreateApiKey();
|
|
1010
1015
|
console.log(` bridgerapi is running${pid ? ` (pid ${pid})` : ""}`);
|
|
1011
|
-
console.log(` Base URL : http://127.0.0.1:${
|
|
1012
|
-
console.log(` API Key :
|
|
1016
|
+
console.log(` Base URL : http://127.0.0.1:${port}/v1`);
|
|
1017
|
+
console.log(` API Key : ${apiKey}`);
|
|
1013
1018
|
if (cfg.backend) console.log(` Backend : ${cfg.backend}`);
|
|
1014
1019
|
} else {
|
|
1015
1020
|
console.log(" bridgerapi is not running.");
|
|
1016
|
-
console.log(" Run: bridgerapi
|
|
1017
|
-
console.log(" Run: bridgerapi start
|
|
1018
|
-
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");
|
|
1019
1024
|
}
|
|
1020
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;
|
|
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();
|
|
1052
|
+
}
|
|
1021
1053
|
function cmdConfig(args) {
|
|
1022
1054
|
const cfg = loadConfig();
|
|
1023
1055
|
if (args[0] === "set") {
|
|
@@ -1037,7 +1069,7 @@ function cmdConfig(args) {
|
|
|
1037
1069
|
process.exit(1);
|
|
1038
1070
|
}
|
|
1039
1071
|
cfg.backend = val;
|
|
1040
|
-
console.log(` backend
|
|
1072
|
+
console.log(` backend -> ${val}`);
|
|
1041
1073
|
} else if (key === "port") {
|
|
1042
1074
|
const p = parseInt(val);
|
|
1043
1075
|
if (isNaN(p) || p < 1 || p > 65535) {
|
|
@@ -1045,7 +1077,7 @@ function cmdConfig(args) {
|
|
|
1045
1077
|
process.exit(1);
|
|
1046
1078
|
}
|
|
1047
1079
|
cfg.port = p;
|
|
1048
|
-
console.log(` port
|
|
1080
|
+
console.log(` port -> ${p}`);
|
|
1049
1081
|
} else {
|
|
1050
1082
|
console.error(` Unknown key: ${key}`);
|
|
1051
1083
|
console.error(` Valid keys: backend, port`);
|
|
@@ -1066,12 +1098,13 @@ function cmdConfig(args) {
|
|
|
1066
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");
|
|
1067
1099
|
console.log(` backend : ${cfg.backend ?? "(auto \u2014 routed by model prefix)"}`);
|
|
1068
1100
|
console.log(` port : ${cfg.port ?? `${DEFAULT_PORT} (default)`}`);
|
|
1101
|
+
console.log(` apiKey : ${cfg.apiKey ?? "(not generated yet \u2014 run: bridgerapi key)"}`);
|
|
1069
1102
|
console.log(` file : ${(0, import_path4.join)((0, import_os4.homedir)(), ".bridgerapi/config.json")}`);
|
|
1070
1103
|
console.log();
|
|
1071
1104
|
console.log(" To change:");
|
|
1072
|
-
console.log(
|
|
1073
|
-
console.log(
|
|
1074
|
-
console.log(
|
|
1105
|
+
console.log(" bridgerapi config set backend=droid");
|
|
1106
|
+
console.log(" bridgerapi config set port=9000");
|
|
1107
|
+
console.log(" bridgerapi config reset");
|
|
1075
1108
|
console.log();
|
|
1076
1109
|
}
|
|
1077
1110
|
async function cmdBackendAdd() {
|
|
@@ -1095,8 +1128,8 @@ async function cmdBackendAdd() {
|
|
|
1095
1128
|
const modeChoice = await ask(" Choose [1/2]: ");
|
|
1096
1129
|
const promptMode = modeChoice === "2" ? "arg" : "stdin";
|
|
1097
1130
|
console.log();
|
|
1098
|
-
console.log(
|
|
1099
|
-
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}");
|
|
1100
1133
|
const argsRaw = await ask(" Args: ");
|
|
1101
1134
|
const args = argsRaw.trim().split(/\s+/).filter(Boolean);
|
|
1102
1135
|
const modelsCmdRaw = await ask(" Args to list models (leave blank to skip): ");
|
|
@@ -1111,11 +1144,8 @@ async function cmdBackendAdd() {
|
|
|
1111
1144
|
else existing.push(def);
|
|
1112
1145
|
saveConfig({ ...cfg, customBackends: existing });
|
|
1113
1146
|
console.log();
|
|
1114
|
-
console.log(`
|
|
1115
|
-
console.log(
|
|
1116
|
-
console.log();
|
|
1117
|
-
console.log(" Example JSON entry in ~/.bridgerapi/config.json:");
|
|
1118
|
-
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.");
|
|
1119
1149
|
console.log();
|
|
1120
1150
|
}
|
|
1121
1151
|
async function cmdBackendList() {
|
|
@@ -1124,34 +1154,34 @@ async function cmdBackendList() {
|
|
|
1124
1154
|
for (const b of BACKENDS) {
|
|
1125
1155
|
if (!b.available()) {
|
|
1126
1156
|
const hint = INSTALL_HINTS[b.name] ?? "not installed";
|
|
1127
|
-
console.log(`
|
|
1128
|
-
console.log(`
|
|
1157
|
+
console.log(` - ${b.name}`);
|
|
1158
|
+
console.log(` -> ${hint}
|
|
1129
1159
|
`);
|
|
1130
1160
|
continue;
|
|
1131
1161
|
}
|
|
1132
|
-
process.stdout.write(`
|
|
1162
|
+
process.stdout.write(` + ${b.name} (discovering models...)\r`);
|
|
1133
1163
|
const modelList = await b.models();
|
|
1134
1164
|
const preview = modelList.slice(0, 6).join(" ");
|
|
1135
1165
|
const extra = modelList.length > 6 ? ` +${modelList.length - 6} more` : "";
|
|
1136
|
-
console.log(`
|
|
1166
|
+
console.log(` + ${b.name} `);
|
|
1137
1167
|
console.log(` ${preview}${extra}
|
|
1138
1168
|
`);
|
|
1139
1169
|
}
|
|
1140
1170
|
}
|
|
1141
|
-
async function cmdChat(
|
|
1171
|
+
async function cmdChat(model, backendFlag) {
|
|
1142
1172
|
const cfg = loadConfig();
|
|
1143
|
-
const activeBackend = backendFlag ?? (
|
|
1173
|
+
const activeBackend = backendFlag ?? (model && BACKENDS.find((b) => b.name === model?.toLowerCase())?.name) ?? cfg.backend;
|
|
1144
1174
|
if (activeBackend) process.env.BRIDGERAPI_BACKEND = activeBackend;
|
|
1145
|
-
const resolvedModel =
|
|
1175
|
+
const resolvedModel = model && BACKENDS.find((b) => b.name === model.toLowerCase()) ? void 0 : model;
|
|
1146
1176
|
const available = BACKENDS.filter((b) => b.available());
|
|
1147
1177
|
if (available.length === 0) {
|
|
1148
1178
|
console.error(" No backends found. Run: bridgerapi to see setup instructions.");
|
|
1149
1179
|
process.exit(1);
|
|
1150
1180
|
}
|
|
1151
1181
|
const fallbackModel = `${activeBackend ?? available[0].name}-default`;
|
|
1152
|
-
const
|
|
1182
|
+
const backend = pickBackend(resolvedModel ?? fallbackModel);
|
|
1153
1183
|
console.log();
|
|
1154
|
-
console.log(` bridgerapi chat \u2014 ${
|
|
1184
|
+
console.log(` bridgerapi chat \u2014 ${backend.name}`);
|
|
1155
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");
|
|
1156
1186
|
console.log(" Type a message and press Enter. Ctrl+C to exit.");
|
|
1157
1187
|
console.log();
|
|
@@ -1172,8 +1202,8 @@ async function cmdChat(model2, backendFlag) {
|
|
|
1172
1202
|
process.stdout.write("\n");
|
|
1173
1203
|
let reply = "";
|
|
1174
1204
|
try {
|
|
1175
|
-
process.stdout.write(`${
|
|
1176
|
-
for await (const chunk2 of
|
|
1205
|
+
process.stdout.write(`${backend.name}: `);
|
|
1206
|
+
for await (const chunk2 of backend.stream(messagesToPrompt(history), resolvedModel ?? fallbackModel)) {
|
|
1177
1207
|
const piece = chunk2.toString("utf8");
|
|
1178
1208
|
process.stdout.write(piece);
|
|
1179
1209
|
reply += piece;
|
|
@@ -1195,79 +1225,89 @@ function showHelp() {
|
|
|
1195
1225
|
|
|
1196
1226
|
Usage:
|
|
1197
1227
|
bridgerapi Interactive setup wizard
|
|
1198
|
-
bridgerapi chat [--model m] Chat in the terminal (routes by model prefix)
|
|
1199
|
-
bridgerapi chat --backend <name> Chat using a specific backend
|
|
1200
1228
|
bridgerapi start [--port n] Start API server in the foreground
|
|
1201
|
-
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
|
|
1202
1234
|
bridgerapi install [--port n] Install as a background service
|
|
1203
1235
|
bridgerapi uninstall Remove background service
|
|
1204
1236
|
bridgerapi status Show service status
|
|
1205
1237
|
bridgerapi config Show saved configuration
|
|
1206
|
-
bridgerapi config set backend=<b> Set default backend
|
|
1238
|
+
bridgerapi config set backend=<b> Set default backend
|
|
1207
1239
|
bridgerapi config set port=<n> Set default port
|
|
1208
1240
|
bridgerapi config reset Clear saved configuration
|
|
1209
1241
|
bridgerapi backend List all backends (built-in + custom)
|
|
1210
1242
|
bridgerapi backend add Add a custom CLI backend interactively
|
|
1211
1243
|
|
|
1212
1244
|
Built-in backends: claude, gemini, codex, copilot, droid
|
|
1213
|
-
Custom backends: add any AI CLI via "bridgerapi backend add"
|
|
1214
1245
|
`.trim());
|
|
1215
1246
|
}
|
|
1216
1247
|
function parseArgs() {
|
|
1217
1248
|
const cfg = loadConfig();
|
|
1218
1249
|
const args = process.argv.slice(2);
|
|
1219
|
-
const
|
|
1220
|
-
let
|
|
1221
|
-
let
|
|
1222
|
-
let
|
|
1223
|
-
const
|
|
1250
|
+
const cmd = args[0] ?? "";
|
|
1251
|
+
let port = cfg.port ?? DEFAULT_PORT;
|
|
1252
|
+
let model;
|
|
1253
|
+
let backend;
|
|
1254
|
+
const rest = [];
|
|
1224
1255
|
for (let i = 1; i < args.length; i++) {
|
|
1225
1256
|
if ((args[i] === "--port" || args[i] === "-p") && args[i + 1]) {
|
|
1226
|
-
|
|
1257
|
+
port = parseInt(args[++i]);
|
|
1227
1258
|
} else if ((args[i] === "--model" || args[i] === "-m") && args[i + 1]) {
|
|
1228
|
-
|
|
1259
|
+
model = args[++i];
|
|
1229
1260
|
} else if ((args[i] === "--backend" || args[i] === "-b") && args[i + 1]) {
|
|
1230
|
-
|
|
1261
|
+
backend = args[++i];
|
|
1231
1262
|
} else {
|
|
1232
|
-
|
|
1263
|
+
rest.push(args[i]);
|
|
1233
1264
|
}
|
|
1234
1265
|
}
|
|
1235
|
-
return { cmd
|
|
1266
|
+
return { cmd, port, model, backend, rest };
|
|
1236
1267
|
}
|
|
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
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
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
|
+
}
|
|
1273
1309
|
}
|
|
1310
|
+
main().catch((err) => {
|
|
1311
|
+
console.error(" Fatal:", err.message);
|
|
1312
|
+
process.exit(1);
|
|
1313
|
+
});
|