152 lines
4.8 KiB
JavaScript
152 lines
4.8 KiB
JavaScript
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const http = require("node:http");
|
|
const WebSocket = require("ws");
|
|
|
|
const {
|
|
buildChatMessages,
|
|
isLoopbackUrl,
|
|
NemotronSidecarClient,
|
|
normalizeOpenAiBaseUrl,
|
|
streamLmStudioChat,
|
|
} = require("../src/utils/localProviders");
|
|
|
|
test("normalizes LM Studio OpenAI-compatible base URLs without selecting models automatically", () => {
|
|
assert.equal(
|
|
normalizeOpenAiBaseUrl("http://127.0.0.1:1234"),
|
|
"http://127.0.0.1:1234/v1",
|
|
);
|
|
assert.equal(
|
|
normalizeOpenAiBaseUrl("http://127.0.0.1:1234/v1/"),
|
|
"http://127.0.0.1:1234/v1",
|
|
);
|
|
});
|
|
|
|
test("detects loopback URLs but does not reject non-local URLs", () => {
|
|
assert.equal(isLoopbackUrl("http://localhost:1234/v1"), true);
|
|
assert.equal(isLoopbackUrl("http://127.0.0.1:1234/v1"), true);
|
|
assert.equal(isLoopbackUrl("http://[::1]:1234/v1"), true);
|
|
assert.equal(isLoopbackUrl("http://192.168.1.40:1234/v1"), false);
|
|
assert.equal(isLoopbackUrl("https://example.com/v1"), false);
|
|
});
|
|
|
|
test("builds OpenAI-compatible image messages for local screenshot analysis", () => {
|
|
const messages = buildChatMessages({
|
|
systemPrompt: "Be useful.",
|
|
history: [{ role: "assistant", content: "Previous answer" }],
|
|
userText: "Analyze this screen",
|
|
imageBase64: "abc123",
|
|
});
|
|
|
|
assert.deepEqual(messages[0], { role: "system", content: "Be useful." });
|
|
assert.equal(messages[1].role, "assistant");
|
|
assert.equal(messages[2].role, "user");
|
|
assert.equal(messages[2].content[0].type, "text");
|
|
assert.equal(messages[2].content[1].type, "image_url");
|
|
assert.equal(
|
|
messages[2].content[1].image_url.url,
|
|
"data:image/jpeg;base64,abc123",
|
|
);
|
|
});
|
|
|
|
test("requires a manually configured LM Studio model id", async () => {
|
|
await assert.rejects(
|
|
() =>
|
|
streamLmStudioChat({
|
|
baseUrl: "http://127.0.0.1:1234/v1",
|
|
model: "",
|
|
messages: [{ role: "user", content: "hello" }],
|
|
}),
|
|
/model id is required/,
|
|
);
|
|
});
|
|
|
|
test("streams LM Studio chat completion tokens from an OpenAI-compatible endpoint", async () => {
|
|
let receivedBody = null;
|
|
const server = http.createServer((req, res) => {
|
|
assert.equal(req.method, "POST");
|
|
assert.equal(req.url, "/v1/chat/completions");
|
|
|
|
let raw = "";
|
|
req.setEncoding("utf8");
|
|
req.on("data", (chunk) => {
|
|
raw += chunk;
|
|
});
|
|
req.on("end", () => {
|
|
receivedBody = JSON.parse(raw);
|
|
res.writeHead(200, {
|
|
"Content-Type": "text/event-stream",
|
|
});
|
|
res.write('data: {"choices":[{"delta":{"content":"hel"}}]}\n\n');
|
|
res.write('data: {"choices":[{"delta":{"content":"lo"}}]}\n\n');
|
|
res.end("data: [DONE]\n\n");
|
|
});
|
|
});
|
|
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
const { port } = server.address();
|
|
const accumulations = [];
|
|
|
|
const fullText = await streamLmStudioChat({
|
|
baseUrl: `http://127.0.0.1:${port}/v1`,
|
|
model: "manual-gemma4",
|
|
messages: [{ role: "user", content: "hello" }],
|
|
onToken: (_token, accumulated) => accumulations.push(accumulated),
|
|
});
|
|
|
|
assert.equal(fullText, "hello");
|
|
assert.deepEqual(accumulations, ["hel", "hello"]);
|
|
assert.equal(receivedBody.model, "manual-gemma4");
|
|
assert.equal(receivedBody.stream, true);
|
|
assert.deepEqual(receivedBody.messages, [{ role: "user", content: "hello" }]);
|
|
|
|
await new Promise((resolve) => server.close(resolve));
|
|
});
|
|
|
|
test("Nemotron sidecar client sends start JSON, binary PCM, and emits final transcripts", async () => {
|
|
const server = new WebSocket.Server({ host: "127.0.0.1", port: 0 });
|
|
await new Promise((resolve) => server.once("listening", resolve));
|
|
|
|
const { port } = server.address();
|
|
const received = [];
|
|
|
|
server.on("connection", (socket) => {
|
|
socket.on("message", (data, isBinary) => {
|
|
if (isBinary) {
|
|
received.push({ isBinary, data: Buffer.from(data) });
|
|
socket.send(JSON.stringify({ type: "final", text: "hello world" }));
|
|
return;
|
|
}
|
|
|
|
received.push({ isBinary, data: JSON.parse(data.toString("utf8")) });
|
|
socket.send(JSON.stringify({ type: "ready" }));
|
|
});
|
|
});
|
|
|
|
const client = new NemotronSidecarClient({
|
|
url: `ws://127.0.0.1:${port}/v1/asr/stream`,
|
|
language: "en-US",
|
|
});
|
|
|
|
const finalPromise = new Promise((resolve) => client.once("final", resolve));
|
|
await client.connect();
|
|
client.sendAudio(Buffer.from([1, 2, 3, 4]));
|
|
|
|
assert.equal(await finalPromise, "hello world");
|
|
assert.deepEqual(received[0], {
|
|
isBinary: false,
|
|
data: {
|
|
type: "start",
|
|
sampleRate: 16000,
|
|
channels: 1,
|
|
encoding: "pcm_s16le",
|
|
language: "en-US",
|
|
},
|
|
});
|
|
assert.equal(received[1].isBinary, true);
|
|
assert.deepEqual([...received[1].data], [1, 2, 3, 4]);
|
|
|
|
client.close();
|
|
await new Promise((resolve) => server.close(resolve));
|
|
});
|