@elvatis_com/openclaw-cli-bridge-elvatis 1.7.5 → 1.8.1
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 +14 -1
- package/SKILL.md +3 -1
- package/index.ts +33 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/proxy-server.ts +70 -2
- package/test/bitnet-proxy.test.ts +195 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> OpenClaw plugin that bridges locally installed AI CLIs (Codex, Gemini, Claude Code) as model providers — with slash commands for instant model switching, restore, health testing, and model listing.
|
|
4
4
|
|
|
5
|
-
**Current version:** `1.
|
|
5
|
+
**Current version:** `1.8.1`
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -57,6 +57,12 @@ All commands use gateway-level `commands.allowFrom` for authorization (`requireA
|
|
|
57
57
|
| `/cli-codex54` | `openai-codex/gpt-5.4` | May require upgraded OAuth scope |
|
|
58
58
|
| `/cli-codex-mini` | `openai-codex/gpt-5.1-codex-mini` | ✅ Tested |
|
|
59
59
|
|
|
60
|
+
**BitNet local inference** (via local proxy → llama-server on 127.0.0.1:8082, no API key):
|
|
61
|
+
|
|
62
|
+
| Command | Model |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `/cli-bitnet` | `vllm/local-bitnet/bitnet-2b` |
|
|
65
|
+
|
|
60
66
|
**Utility:**
|
|
61
67
|
|
|
62
68
|
| Command | What it does |
|
|
@@ -362,6 +368,13 @@ npm test # vitest run (83 tests)
|
|
|
362
368
|
|
|
363
369
|
## Changelog
|
|
364
370
|
|
|
371
|
+
### v1.8.1
|
|
372
|
+
- **fix:** `--now` flag now works when followed by additional text (e.g. `/cli-bitnet --now hello`) — was using `===` instead of `startsWith`.
|
|
373
|
+
|
|
374
|
+
### v1.8.0
|
|
375
|
+
- **feat:** BitNet local inference — `local-bitnet/bitnet-2b` routes to llama-server on 127.0.0.1:8082. No API key, no internet, pure CPU inference (2.87 tok/s on i7-6700K). Use `/cli-bitnet` to switch.
|
|
376
|
+
- **feat:** `/bridge-status` shows BitNet server health as 5th provider.
|
|
377
|
+
|
|
365
378
|
### v1.7.5
|
|
366
379
|
- **chore:** Re-published to ClawHub with correct display name "OpenClaw CLI Bridge"
|
|
367
380
|
|
package/SKILL.md
CHANGED
|
@@ -24,6 +24,7 @@ Registers `openai-codex` provider from existing `~/.codex/auth.json` tokens. No
|
|
|
24
24
|
Local OpenAI-compatible HTTP proxy (`127.0.0.1:31337`) routes vllm model calls to CLI subprocesses:
|
|
25
25
|
- `vllm/cli-gemini/gemini-2.5-pro` / `gemini-2.5-flash` / `gemini-3-pro`
|
|
26
26
|
- `vllm/cli-claude/claude-sonnet-4-6` / `claude-opus-4-6` / `claude-haiku-4-5`
|
|
27
|
+
- `vllm/local-bitnet/bitnet-2b` → BitNet llama-server on 127.0.0.1:8082
|
|
27
28
|
|
|
28
29
|
Prompts go via stdin/tmpfile — never as CLI args (prevents `E2BIG` for long sessions).
|
|
29
30
|
|
|
@@ -40,6 +41,7 @@ Six instant model-switch commands (authorized senders only):
|
|
|
40
41
|
| `/cli-gemini3` | `vllm/cli-gemini/gemini-3-pro` |
|
|
41
42
|
| `/cli-codex` | `openai-codex/gpt-5.3-codex` |
|
|
42
43
|
| `/cli-codex54` | `openai-codex/gpt-5.4` |
|
|
44
|
+
| `/cli-bitnet` | `vllm/local-bitnet/bitnet-2b` |
|
|
43
45
|
| `/cli-back` | Restore previous model |
|
|
44
46
|
| `/cli-test [model]` | Health check (no model switch) |
|
|
45
47
|
|
|
@@ -66,4 +68,4 @@ On gateway restart, if any session has expired, a **WhatsApp alert** is sent aut
|
|
|
66
68
|
|
|
67
69
|
See `README.md` for full configuration reference and architecture diagram.
|
|
68
70
|
|
|
69
|
-
**Version:** 1.
|
|
71
|
+
**Version:** 1.8.1
|
package/index.ts
CHANGED
|
@@ -705,6 +705,25 @@ function readCurrentModel(): string | null {
|
|
|
705
705
|
}
|
|
706
706
|
}
|
|
707
707
|
|
|
708
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
709
|
+
// BitNet server health check
|
|
710
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
711
|
+
async function checkBitNetServer(url = "http://127.0.0.1:8082"): Promise<boolean> {
|
|
712
|
+
return new Promise((resolve) => {
|
|
713
|
+
const target = new URL("/v1/models", url);
|
|
714
|
+
const req = http.get(
|
|
715
|
+
{ hostname: target.hostname, port: parseInt(target.port), path: target.pathname, timeout: 3_000 },
|
|
716
|
+
(res) => {
|
|
717
|
+
let data = "";
|
|
718
|
+
res.on("data", (c: Buffer) => (data += c));
|
|
719
|
+
res.on("end", () => resolve(res.statusCode === 200));
|
|
720
|
+
}
|
|
721
|
+
);
|
|
722
|
+
req.on("error", () => resolve(false));
|
|
723
|
+
req.on("timeout", () => { req.destroy(); resolve(false); });
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
708
727
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
709
728
|
// Phase 3: model command table
|
|
710
729
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -720,6 +739,8 @@ const CLI_MODEL_COMMANDS = [
|
|
|
720
739
|
// ── Codex CLI (openai-codex provider, OAuth auth) ────────────────────────────
|
|
721
740
|
{ name: "cli-codex", model: "openai-codex/gpt-5.3-codex", description: "GPT-5.3 Codex (Codex CLI auth)", label: "GPT-5.3 Codex" },
|
|
722
741
|
{ name: "cli-codex54", model: "openai-codex/gpt-5.4", description: "GPT-5.4 (Codex CLI auth)", label: "GPT-5.4" },
|
|
742
|
+
// ── BitNet local inference (via local proxy → llama-server) ─────────────────
|
|
743
|
+
{ name: "cli-bitnet", model: "vllm/local-bitnet/bitnet-2b", description: "BitNet b1.58 2B (local CPU, no API key)", label: "BitNet 2B (local)" },
|
|
723
744
|
] as const;
|
|
724
745
|
|
|
725
746
|
/** Default model used by /cli-test when no arg is given */
|
|
@@ -918,7 +939,7 @@ function proxyTestRequest(
|
|
|
918
939
|
const plugin = {
|
|
919
940
|
id: "openclaw-cli-bridge-elvatis",
|
|
920
941
|
name: "OpenClaw CLI Bridge",
|
|
921
|
-
version: "1.
|
|
942
|
+
version: "1.8.1",
|
|
922
943
|
description:
|
|
923
944
|
"Phase 1: openai-codex auth bridge. " +
|
|
924
945
|
"Phase 2: HTTP proxy for gemini/claude CLIs. " +
|
|
@@ -1387,7 +1408,7 @@ const plugin = {
|
|
|
1387
1408
|
acceptsArgs: true,
|
|
1388
1409
|
requireAuth: false,
|
|
1389
1410
|
handler: async (ctx: PluginCommandContext): Promise<PluginCommandResult> => {
|
|
1390
|
-
const forceNow = (ctx.args ?? "").trim().toLowerCase()
|
|
1411
|
+
const forceNow = (ctx.args ?? "").trim().toLowerCase().startsWith("--now");
|
|
1391
1412
|
api.logger.info(`[cli-bridge] /${name} by ${ctx.senderId ?? "?"} forceNow=${forceNow}`);
|
|
1392
1413
|
return switchModel(api, model, label, forceNow);
|
|
1393
1414
|
},
|
|
@@ -2152,6 +2173,16 @@ const plugin = {
|
|
|
2152
2173
|
lines.push("");
|
|
2153
2174
|
}
|
|
2154
2175
|
|
|
2176
|
+
// ── BitNet local inference ──────────────────────────────────────────────
|
|
2177
|
+
const bitnetOk = await checkBitNetServer();
|
|
2178
|
+
if (bitnetOk) {
|
|
2179
|
+
lines.push(`✅ *BitNet (local)* — running at 127.0.0.1:8082`);
|
|
2180
|
+
lines.push(` Models: local-bitnet/bitnet-2b`);
|
|
2181
|
+
} else {
|
|
2182
|
+
lines.push(`❌ *BitNet (local)* — not running (\`sudo systemctl start bitnet-server\`)`);
|
|
2183
|
+
}
|
|
2184
|
+
lines.push("");
|
|
2185
|
+
|
|
2155
2186
|
lines.push(`🔌 Proxy: \`127.0.0.1:${port}\``);
|
|
2156
2187
|
return { text: lines.join("\n") };
|
|
2157
2188
|
},
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-cli-bridge-elvatis",
|
|
3
3
|
"name": "OpenClaw CLI Bridge",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.8.1",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Phase 1: openai-codex auth bridge. Phase 2: local HTTP proxy routing model calls through gemini/claude CLIs (vllm provider).",
|
|
7
7
|
"providers": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elvatis_com/openclaw-cli-bridge-elvatis",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "Bridges gemini, claude, and codex CLI tools as OpenClaw model providers. Reads existing CLI auth without re-login.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"openclaw": {
|
package/src/proxy-server.ts
CHANGED
|
@@ -69,6 +69,8 @@ export interface ProxyServerOptions {
|
|
|
69
69
|
};
|
|
70
70
|
/** Plugin version string for the status page */
|
|
71
71
|
version?: string;
|
|
72
|
+
/** Returns the BitNet llama-server base URL (default: http://127.0.0.1:8082) */
|
|
73
|
+
getBitNetServerUrl?: () => string;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
/** Available CLI bridge models for GET /v1/models */
|
|
@@ -104,6 +106,8 @@ export const CLI_MODELS = [
|
|
|
104
106
|
{ id: "web-chatgpt/o4-mini", name: "o4-mini (web session)", contextWindow: 200_000, maxTokens: 100_000 },
|
|
105
107
|
{ id: "web-chatgpt/gpt-5", name: "GPT-5 (web session)", contextWindow: 1_047_576, maxTokens: 32_768 },
|
|
106
108
|
{ id: "web-chatgpt/gpt-5-mini", name: "GPT-5 Mini (web session)", contextWindow: 1_047_576, maxTokens: 32_768 },
|
|
109
|
+
// ── Local BitNet inference ──────────────────────────────────────────────────
|
|
110
|
+
{ id: "local-bitnet/bitnet-2b", name: "BitNet b1.58 2B (local CPU inference)", contextWindow: 4_096, maxTokens: 2_048 },
|
|
107
111
|
];
|
|
108
112
|
|
|
109
113
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -206,6 +210,7 @@ async function handleRequest(
|
|
|
206
210
|
|
|
207
211
|
const cliModels = CLI_MODELS.filter(m => m.id.startsWith("cli-"));
|
|
208
212
|
const webModels = CLI_MODELS.filter(m => m.id.startsWith("web-"));
|
|
213
|
+
const localModels = CLI_MODELS.filter(m => m.id.startsWith("local-"));
|
|
209
214
|
const modelList = (models: typeof CLI_MODELS) =>
|
|
210
215
|
models.map(m => `<li style="margin:2px 0;font-size:13px;color:#d1d5db"><code style="color:#93c5fd">${m.id}</code></li>`).join("");
|
|
211
216
|
|
|
@@ -259,6 +264,10 @@ async function handleRequest(
|
|
|
259
264
|
<div class="card-header">Web Session Models (${webModels.length})</div>
|
|
260
265
|
<ul>${modelList(webModels)}</ul>
|
|
261
266
|
</div>
|
|
267
|
+
<div class="card">
|
|
268
|
+
<div class="card-header">Local Models (${localModels.length})</div>
|
|
269
|
+
<ul>${modelList(localModels)}</ul>
|
|
270
|
+
</div>
|
|
262
271
|
</div>
|
|
263
272
|
|
|
264
273
|
<p class="footer">openclaw-cli-bridge-elvatis v${version} · <a href="/v1/models" style="color:#4b5563">/v1/models</a> · <a href="/health" style="color:#4b5563">/health</a></p>
|
|
@@ -284,7 +293,7 @@ async function handleRequest(
|
|
|
284
293
|
owned_by: "openclaw-cli-bridge",
|
|
285
294
|
// CLI-proxy models stream plain text — no tool/function call support
|
|
286
295
|
capabilities: {
|
|
287
|
-
tools: !(m.id.startsWith("cli-gemini/") || m.id.startsWith("cli-claude/")),
|
|
296
|
+
tools: !(m.id.startsWith("cli-gemini/") || m.id.startsWith("cli-claude/") || m.id.startsWith("local-bitnet/")),
|
|
288
297
|
},
|
|
289
298
|
})),
|
|
290
299
|
})
|
|
@@ -332,7 +341,7 @@ async function handleRequest(
|
|
|
332
341
|
// CLI-proxy models (cli-gemini/*, cli-claude/*) are plain text completions —
|
|
333
342
|
// they cannot process tool/function call schemas. Return a clear 400 so
|
|
334
343
|
// OpenClaw can surface a meaningful error instead of getting a garbled response.
|
|
335
|
-
const isCliModel = model.startsWith("cli-gemini/") || model.startsWith("cli-claude/");
|
|
344
|
+
const isCliModel = model.startsWith("cli-gemini/") || model.startsWith("cli-claude/") || model.startsWith("local-bitnet/");
|
|
336
345
|
if (hasTools && isCliModel) {
|
|
337
346
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
338
347
|
res.end(JSON.stringify({
|
|
@@ -549,6 +558,65 @@ async function handleRequest(
|
|
|
549
558
|
}
|
|
550
559
|
// ─────────────────────────────────────────────────────────────────────────
|
|
551
560
|
|
|
561
|
+
// ── BitNet local inference routing ────────────────────────────────────────
|
|
562
|
+
if (model.startsWith("local-bitnet/")) {
|
|
563
|
+
const bitnetUrl = opts.getBitNetServerUrl?.() ?? "http://127.0.0.1:8082";
|
|
564
|
+
const timeoutMs = opts.timeoutMs ?? 120_000;
|
|
565
|
+
const requestBody = JSON.stringify(parsed);
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
const targetUrl = new URL("/v1/chat/completions", bitnetUrl);
|
|
569
|
+
const proxyRes = await new Promise<http.IncomingMessage>((resolve, reject) => {
|
|
570
|
+
const proxyReq = http.request(
|
|
571
|
+
{
|
|
572
|
+
hostname: targetUrl.hostname,
|
|
573
|
+
port: parseInt(targetUrl.port),
|
|
574
|
+
path: targetUrl.pathname,
|
|
575
|
+
method: "POST",
|
|
576
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(requestBody) },
|
|
577
|
+
timeout: timeoutMs,
|
|
578
|
+
},
|
|
579
|
+
resolve
|
|
580
|
+
);
|
|
581
|
+
proxyReq.on("error", reject);
|
|
582
|
+
proxyReq.on("timeout", () => { proxyReq.destroy(new Error("BitNet request timed out")); });
|
|
583
|
+
proxyReq.write(requestBody);
|
|
584
|
+
proxyReq.end();
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// Forward status + headers
|
|
588
|
+
const fwdHeaders: Record<string, string> = { ...corsHeaders() };
|
|
589
|
+
const ct = proxyRes.headers["content-type"];
|
|
590
|
+
if (ct) fwdHeaders["Content-Type"] = ct;
|
|
591
|
+
if (stream) {
|
|
592
|
+
fwdHeaders["Cache-Control"] = "no-cache";
|
|
593
|
+
fwdHeaders["Connection"] = "keep-alive";
|
|
594
|
+
}
|
|
595
|
+
res.writeHead(proxyRes.statusCode ?? 200, fwdHeaders);
|
|
596
|
+
proxyRes.pipe(res);
|
|
597
|
+
} catch (err) {
|
|
598
|
+
const msg = (err as Error).message;
|
|
599
|
+
if (msg.includes("ECONNREFUSED") || msg.includes("ECONNRESET") || msg.includes("ENOTFOUND")) {
|
|
600
|
+
res.writeHead(503, { "Content-Type": "application/json", ...corsHeaders() });
|
|
601
|
+
res.end(JSON.stringify({
|
|
602
|
+
error: {
|
|
603
|
+
message: "BitNet server not running. Start with: sudo systemctl start bitnet-server",
|
|
604
|
+
type: "bitnet_error",
|
|
605
|
+
code: "bitnet_unavailable",
|
|
606
|
+
},
|
|
607
|
+
}));
|
|
608
|
+
} else {
|
|
609
|
+
opts.warn(`[cli-bridge] BitNet error for ${model}: ${msg}`);
|
|
610
|
+
if (!res.headersSent) {
|
|
611
|
+
res.writeHead(500, { "Content-Type": "application/json", ...corsHeaders() });
|
|
612
|
+
res.end(JSON.stringify({ error: { message: msg, type: "bitnet_error" } }));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
619
|
+
|
|
552
620
|
// ── CLI runner routing (Gemini / Claude Code) ─────────────────────────────
|
|
553
621
|
let content: string;
|
|
554
622
|
try {
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* test/bitnet-proxy.test.ts
|
|
3
|
+
*
|
|
4
|
+
* Tests for BitNet local inference routing in the cli-bridge proxy.
|
|
5
|
+
* Spins up a mock llama-server and validates:
|
|
6
|
+
* - 503 when BitNet server is unreachable
|
|
7
|
+
* - Successful forward (non-streaming)
|
|
8
|
+
* - Tools rejection (400)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
12
|
+
import http from "node:http";
|
|
13
|
+
import type { AddressInfo } from "node:net";
|
|
14
|
+
import { startProxyServer, CLI_MODELS } from "../src/proxy-server.js";
|
|
15
|
+
|
|
16
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
// Mock llama-server — responds to POST /v1/chat/completions
|
|
18
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
let mockLlamaServer: http.Server;
|
|
21
|
+
let mockLlamaPort: number;
|
|
22
|
+
|
|
23
|
+
function startMockLlamaServer(): Promise<void> {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
mockLlamaServer = http.createServer((req, res) => {
|
|
26
|
+
if (req.url === "/v1/models" && req.method === "GET") {
|
|
27
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
28
|
+
res.end(JSON.stringify({ data: [{ id: "bitnet-2b" }] }));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (req.url === "/v1/chat/completions" && req.method === "POST") {
|
|
32
|
+
const chunks: Buffer[] = [];
|
|
33
|
+
req.on("data", (d: Buffer) => chunks.push(d));
|
|
34
|
+
req.on("end", () => {
|
|
35
|
+
const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
36
|
+
const lastMsg = body.messages?.[body.messages.length - 1]?.content ?? "";
|
|
37
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
38
|
+
res.end(JSON.stringify({
|
|
39
|
+
id: "chatcmpl-bitnet-mock",
|
|
40
|
+
object: "chat.completion",
|
|
41
|
+
created: Math.floor(Date.now() / 1000),
|
|
42
|
+
model: "bitnet-2b",
|
|
43
|
+
choices: [{ index: 0, message: { role: "assistant", content: `bitnet echo: ${lastMsg}` }, finish_reason: "stop" }],
|
|
44
|
+
usage: { prompt_tokens: 4, completion_tokens: 6, total_tokens: 10 },
|
|
45
|
+
}));
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
res.writeHead(404);
|
|
50
|
+
res.end();
|
|
51
|
+
});
|
|
52
|
+
mockLlamaServer.listen(0, "127.0.0.1", () => {
|
|
53
|
+
mockLlamaPort = (mockLlamaServer.address() as AddressInfo).port;
|
|
54
|
+
resolve();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
60
|
+
// HTTP helpers
|
|
61
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
async function httpPost(
|
|
64
|
+
url: string,
|
|
65
|
+
body: unknown,
|
|
66
|
+
headers: Record<string, string> = {}
|
|
67
|
+
): Promise<{ status: number; body: unknown }> {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const data = JSON.stringify(body);
|
|
70
|
+
const urlObj = new URL(url);
|
|
71
|
+
const req = http.request(
|
|
72
|
+
{
|
|
73
|
+
hostname: urlObj.hostname,
|
|
74
|
+
port: parseInt(urlObj.port),
|
|
75
|
+
path: urlObj.pathname,
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: {
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
"Content-Length": Buffer.byteLength(data),
|
|
80
|
+
...headers,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
(res) => {
|
|
84
|
+
let resp = "";
|
|
85
|
+
res.on("data", (c) => (resp += c));
|
|
86
|
+
res.on("end", () => {
|
|
87
|
+
try { resolve({ status: res.statusCode ?? 0, body: JSON.parse(resp) }); }
|
|
88
|
+
catch { resolve({ status: res.statusCode ?? 0, body: resp }); }
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
req.on("error", reject);
|
|
93
|
+
req.write(data);
|
|
94
|
+
req.end();
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
99
|
+
// Setup: two proxy servers
|
|
100
|
+
// - withBitNet: points to mock llama-server
|
|
101
|
+
// - noBitNet: points to unreachable port (503 expected)
|
|
102
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
const TEST_KEY = "test-bitnet-key";
|
|
105
|
+
let serverWith: http.Server;
|
|
106
|
+
let serverNo: http.Server;
|
|
107
|
+
let urlWith: string;
|
|
108
|
+
let urlNo: string;
|
|
109
|
+
|
|
110
|
+
beforeAll(async () => {
|
|
111
|
+
await startMockLlamaServer();
|
|
112
|
+
|
|
113
|
+
serverWith = await startProxyServer({
|
|
114
|
+
port: 0,
|
|
115
|
+
apiKey: TEST_KEY,
|
|
116
|
+
log: () => {},
|
|
117
|
+
warn: () => {},
|
|
118
|
+
getBitNetServerUrl: () => `http://127.0.0.1:${mockLlamaPort}`,
|
|
119
|
+
});
|
|
120
|
+
const addrWith = serverWith.address() as AddressInfo;
|
|
121
|
+
urlWith = `http://127.0.0.1:${addrWith.port}`;
|
|
122
|
+
|
|
123
|
+
serverNo = await startProxyServer({
|
|
124
|
+
port: 0,
|
|
125
|
+
apiKey: TEST_KEY,
|
|
126
|
+
log: () => {},
|
|
127
|
+
warn: () => {},
|
|
128
|
+
getBitNetServerUrl: () => `http://127.0.0.1:1`, // unreachable
|
|
129
|
+
});
|
|
130
|
+
const addrNo = serverNo.address() as AddressInfo;
|
|
131
|
+
urlNo = `http://127.0.0.1:${addrNo.port}`;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
afterAll(async () => {
|
|
135
|
+
await new Promise<void>((r) => serverWith.close(() => r()));
|
|
136
|
+
await new Promise<void>((r) => serverNo.close(() => r()));
|
|
137
|
+
await new Promise<void>((r) => mockLlamaServer.close(() => r()));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
141
|
+
// Tests
|
|
142
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
describe("CLI_MODELS includes BitNet", () => {
|
|
145
|
+
it("has local-bitnet/bitnet-2b in the model list", () => {
|
|
146
|
+
const bitnet = CLI_MODELS.filter((m) => m.id.startsWith("local-bitnet/"));
|
|
147
|
+
expect(bitnet).toHaveLength(1);
|
|
148
|
+
expect(bitnet[0].id).toBe("local-bitnet/bitnet-2b");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("POST /v1/chat/completions — BitNet routing", () => {
|
|
153
|
+
const auth = { Authorization: `Bearer ${TEST_KEY}` };
|
|
154
|
+
|
|
155
|
+
it("returns 503 when BitNet server is unreachable", async () => {
|
|
156
|
+
const { status, body } = await httpPost(
|
|
157
|
+
`${urlNo}/v1/chat/completions`,
|
|
158
|
+
{ model: "local-bitnet/bitnet-2b", messages: [{ role: "user", content: "Hi" }] },
|
|
159
|
+
auth
|
|
160
|
+
);
|
|
161
|
+
expect(status).toBe(503);
|
|
162
|
+
const b = body as { error: { code: string; message: string } };
|
|
163
|
+
expect(b.error.code).toBe("bitnet_unavailable");
|
|
164
|
+
expect(b.error.message).toContain("BitNet server not running");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("forwards request to mock llama-server (non-streaming)", async () => {
|
|
168
|
+
const { status, body } = await httpPost(
|
|
169
|
+
`${urlWith}/v1/chat/completions`,
|
|
170
|
+
{ model: "local-bitnet/bitnet-2b", messages: [{ role: "user", content: "Hello BitNet" }], stream: false },
|
|
171
|
+
auth
|
|
172
|
+
);
|
|
173
|
+
expect(status).toBe(200);
|
|
174
|
+
const b = body as {
|
|
175
|
+
choices: Array<{ message: { content: string }; finish_reason: string }>;
|
|
176
|
+
};
|
|
177
|
+
expect(b.choices[0].message.content).toContain("Hello BitNet");
|
|
178
|
+
expect(b.choices[0].finish_reason).toBe("stop");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("rejects tool calls with 400", async () => {
|
|
182
|
+
const { status, body } = await httpPost(
|
|
183
|
+
`${urlWith}/v1/chat/completions`,
|
|
184
|
+
{
|
|
185
|
+
model: "local-bitnet/bitnet-2b",
|
|
186
|
+
messages: [{ role: "user", content: "use tools" }],
|
|
187
|
+
tools: [{ type: "function", function: { name: "test", parameters: {} } }],
|
|
188
|
+
},
|
|
189
|
+
auth
|
|
190
|
+
);
|
|
191
|
+
expect(status).toBe(400);
|
|
192
|
+
const b = body as { error: { code: string } };
|
|
193
|
+
expect(b.error.code).toBe("tools_not_supported");
|
|
194
|
+
});
|
|
195
|
+
});
|