feat: add ignore-certificate-errors option for self-signed TLS
This commit is contained in:
@@ -11,6 +11,9 @@
|
|||||||
<sdpi-item label="API Key">
|
<sdpi-item label="API Key">
|
||||||
<sdpi-password setting="apiKey" placeholder="Optional" />
|
<sdpi-password setting="apiKey" placeholder="Optional" />
|
||||||
</sdpi-item>
|
</sdpi-item>
|
||||||
|
<sdpi-item>
|
||||||
|
<sdpi-checkbox setting="insecure" label="Ignore certificate errors (self-signed TLS)"></sdpi-checkbox>
|
||||||
|
</sdpi-item>
|
||||||
<sdpi-item label="GPU">
|
<sdpi-item label="GPU">
|
||||||
<sdpi-select setting="gpuId" datasource="gpus" loading="Loading GPUs…" hot-reload default="all" placeholder="Select a GPU" />
|
<sdpi-select setting="gpuId" datasource="gpus" loading="Loading GPUs…" hot-reload default="all" placeholder="Select a GPU" />
|
||||||
</sdpi-item>
|
</sdpi-item>
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
<sdpi-item label="API Key">
|
<sdpi-item label="API Key">
|
||||||
<sdpi-password setting="apiKey" placeholder="Optional" />
|
<sdpi-password setting="apiKey" placeholder="Optional" />
|
||||||
</sdpi-item>
|
</sdpi-item>
|
||||||
|
<sdpi-item>
|
||||||
|
<sdpi-checkbox setting="insecure" label="Ignore certificate errors (self-signed TLS)"></sdpi-checkbox>
|
||||||
|
</sdpi-item>
|
||||||
<sdpi-item label="Display">
|
<sdpi-item label="Display">
|
||||||
<sdpi-select setting="display" default="count" placeholder="Select a display mode">
|
<sdpi-select setting="display" default="count" placeholder="Select a display mode">
|
||||||
<option value="count">Request count + activity</option>
|
<option value="count">Request count + activity</option>
|
||||||
|
|||||||
Generated
+11
-1
@@ -8,7 +8,8 @@
|
|||||||
"name": "llama-watch",
|
"name": "llama-watch",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elgato/streamdeck": "^2.1.1"
|
"@elgato/streamdeck": "^2.1.1",
|
||||||
|
"undici": "^6.28.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@elgato/cli": "^1.8.1",
|
"@elgato/cli": "^1.8.1",
|
||||||
@@ -2477,6 +2478,15 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici": {
|
||||||
|
"version": "6.28.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
|
||||||
|
"integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.20.0",
|
"version": "6.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||||
|
|||||||
+2
-1
@@ -23,6 +23,7 @@
|
|||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elgato/streamdeck": "^2.1.1"
|
"@elgato/streamdeck": "^2.1.1",
|
||||||
|
"undici": "^6.28.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { fetchWith } from "./http";
|
||||||
import { type LlamaSwapConfig } from "./util";
|
import { type LlamaSwapConfig } from "./util";
|
||||||
import { parseSse, type SseMessage } from "./sse";
|
import { parseSse, type SseMessage } from "./sse";
|
||||||
import { type FeedEvent, type InflightRequest, type ModelState } from "./inflight-tracker";
|
import { type FeedEvent, type InflightRequest, type ModelState } from "./inflight-tracker";
|
||||||
@@ -80,7 +81,7 @@ export class EventFeed {
|
|||||||
try {
|
try {
|
||||||
const headers: Record<string, string> = { Accept: "text/event-stream" };
|
const headers: Record<string, string> = { Accept: "text/event-stream" };
|
||||||
if (this.cfg.apiKey) headers["Authorization"] = `Bearer ${this.cfg.apiKey}`;
|
if (this.cfg.apiKey) headers["Authorization"] = `Bearer ${this.cfg.apiKey}`;
|
||||||
const res = await fetch(`${this.cfg.baseUrl}/api/events`, {
|
const res = await fetchWith(this.cfg, `${this.cfg.baseUrl}/api/events`, {
|
||||||
headers,
|
headers,
|
||||||
signal: this.controller.signal,
|
signal: this.controller.signal,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Agent, fetch as undiciFetch } from "undici";
|
||||||
|
import { type LlamaSwapConfig } from "./util";
|
||||||
|
|
||||||
|
const INSECURE_AGENT = new Agent({ connect: { rejectUnauthorized: false } });
|
||||||
|
|
||||||
|
export function fetchWith(cfg: LlamaSwapConfig, input: string, init?: RequestInit): Promise<Response> {
|
||||||
|
if (cfg.insecure !== true) return fetch(input, init);
|
||||||
|
return undiciFetch(input, { ...(init ?? {}), dispatcher: INSECURE_AGENT }) as unknown as Promise<Response>;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { fetchWith } from "./http";
|
||||||
import { type LlamaSwapConfig } from "./util";
|
import { type LlamaSwapConfig } from "./util";
|
||||||
|
|
||||||
const TIMEOUT_MS = 5000;
|
const TIMEOUT_MS = 5000;
|
||||||
@@ -14,7 +15,7 @@ function headersFor(cfg: LlamaSwapConfig): Record<string, string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchMetrics(cfg: LlamaSwapConfig): Promise<string> {
|
export async function fetchMetrics(cfg: LlamaSwapConfig): Promise<string> {
|
||||||
const res = await fetch(`${cfg.baseUrl}/metrics`, {
|
const res = await fetchWith(cfg, `${cfg.baseUrl}/metrics`, {
|
||||||
headers: headersFor(cfg),
|
headers: headersFor(cfg),
|
||||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||||
});
|
});
|
||||||
@@ -23,7 +24,7 @@ export async function fetchMetrics(cfg: LlamaSwapConfig): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchModels(cfg: LlamaSwapConfig): Promise<ModelInfo[]> {
|
export async function fetchModels(cfg: LlamaSwapConfig): Promise<ModelInfo[]> {
|
||||||
const res = await fetch(`${cfg.baseUrl}/v1/models`, {
|
const res = await fetchWith(cfg, `${cfg.baseUrl}/v1/models`, {
|
||||||
headers: headersFor(cfg),
|
headers: headersFor(cfg),
|
||||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||||
});
|
});
|
||||||
|
|||||||
+5
-1
@@ -16,7 +16,11 @@ class Runtime {
|
|||||||
private listeners = new Set<() => void>();
|
private listeners = new Set<() => void>();
|
||||||
|
|
||||||
ensureConnections(cfg: LlamaSwapConfig): void {
|
ensureConnections(cfg: LlamaSwapConfig): void {
|
||||||
const changed = !this.cfg || this.cfg.baseUrl !== cfg.baseUrl || this.cfg.apiKey !== cfg.apiKey;
|
const changed =
|
||||||
|
!this.cfg ||
|
||||||
|
this.cfg.baseUrl !== cfg.baseUrl ||
|
||||||
|
this.cfg.apiKey !== cfg.apiKey ||
|
||||||
|
this.cfg.insecure !== cfg.insecure;
|
||||||
if (changed) {
|
if (changed) {
|
||||||
this.feed?.stop();
|
this.feed?.stop();
|
||||||
this.poller?.stop();
|
this.poller?.stop();
|
||||||
|
|||||||
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import { fetchWith } from "./http";
|
||||||
import { type LlamaSwapConfig } from "./util";
|
import { type LlamaSwapConfig } from "./util";
|
||||||
|
|
||||||
export interface UsageStats {
|
export interface UsageStats {
|
||||||
@@ -24,7 +25,7 @@ export async function fetchStats(cfg: LlamaSwapConfig, modelId: string): Promise
|
|||||||
const query = modelId === "all" ? "" : `?model=${encodeURIComponent(modelId)}`;
|
const query = modelId === "all" ? "" : `?model=${encodeURIComponent(modelId)}`;
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
if (cfg.apiKey) headers["Authorization"] = `Bearer ${cfg.apiKey}`;
|
if (cfg.apiKey) headers["Authorization"] = `Bearer ${cfg.apiKey}`;
|
||||||
const res = await fetch(`${cfg.baseUrl}/api/metrics/stats${query}`, {
|
const res = await fetchWith(cfg, `${cfg.baseUrl}/api/metrics/stats${query}`, {
|
||||||
headers,
|
headers,
|
||||||
signal: AbortSignal.timeout(5000),
|
signal: AbortSignal.timeout(5000),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,17 +5,20 @@ export const DEFAULT_BASE_URL = "http://localhost:9292";
|
|||||||
export type CfgSettings = {
|
export type CfgSettings = {
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
|
insecure?: boolean;
|
||||||
} & JsonObject;
|
} & JsonObject;
|
||||||
|
|
||||||
export interface LlamaSwapConfig {
|
export interface LlamaSwapConfig {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
|
insecure?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cfgFromSettings(settings: CfgSettings): LlamaSwapConfig {
|
export function cfgFromSettings(settings: CfgSettings): LlamaSwapConfig {
|
||||||
return {
|
return {
|
||||||
baseUrl: normalizeBaseUrl(settings.baseUrl ?? DEFAULT_BASE_URL),
|
baseUrl: normalizeBaseUrl(settings.baseUrl ?? DEFAULT_BASE_URL),
|
||||||
apiKey: settings.apiKey || undefined,
|
apiKey: settings.apiKey || undefined,
|
||||||
|
insecure: settings.insecure === true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+19
@@ -0,0 +1,19 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDCTCCAfGgAwIBAgIUXQ30Z78Fhhti2Ct6DXczY/jvDjcwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDgyNzIxMTAyNloXDTM2MDgy
|
||||||
|
NDIxMTAyNlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
||||||
|
AAOCAQ8AMIIBCgKCAQEAtJNuhOYp4Qi7W17LRvph2EZH7WfQsbELROrT3ijJ2ys0
|
||||||
|
iA+Bee0ATpc/1R+NgSGfQZsPlAYh2TlYK0UWBIA1BGKm5tSHpBvfxs+GXcgmFM0k
|
||||||
|
nWFE4Kof6d8zvcd3l7aU+8w8Tz/LnhLpaYQiixZ4ZIoAC1SEWKO0wm+AoMSSeLjH
|
||||||
|
CBaGmLuD1MQ+I6kZPwDK2YluZ28LHMH9ZCbgfVKXR0FVjgF0fEw6645IzgWGUznN
|
||||||
|
8pcEisVG2Veyr83Q7uqEL3CtvrdYFjgFJP0Qzez2zgCGXKB+tiBsWIzL2rwVnDcV
|
||||||
|
ss2bwAVz9JMUxaj8aoMq0Bx5hJkzK9JipGRB9C58CQIDAQABo1MwUTAdBgNVHQ4E
|
||||||
|
FgQUCtzsN0GW6A2iKrJF28+UVJOY7zkwHwYDVR0jBBgwFoAUCtzsN0GW6A2iKrJF
|
||||||
|
28+UVJOY7zkwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEApZfd
|
||||||
|
QLXrtRfu6NG1BPfzaROjwTLs5O0tU8+/SSyi3WjoIZmYUBkP7nJm/sR2ZxpSUEfq
|
||||||
|
OFZwux8bDJTccS9Au0/OR9vYbHWmDoY1e14v2GehNRWXz8vvaD3AURluAYXcmgol
|
||||||
|
5WOnSB4W8rp6A5gEKX7n4hsHrkUx/Mt+uSg7KZv/fFnwQqyu9OthPKVNV1v79dgm
|
||||||
|
C0ZUNhpaDDX5Ae+khY88kGADwCKY9BTNzftQG0/4ZJgu7O34eHNgC+2MAgXDtLxm
|
||||||
|
TIk3XSEaFDoT+HOX38Lh3JYnzJCFO59tQfMMGrPuWfgOISxCe5tkZ22zTtPId+wB
|
||||||
|
+TptauhacNtwwpc1Jw==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
Vendored
+28
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC0k26E5inhCLtb
|
||||||
|
XstG+mHYRkftZ9CxsQtE6tPeKMnbKzSID4F57QBOlz/VH42BIZ9Bmw+UBiHZOVgr
|
||||||
|
RRYEgDUEYqbm1IekG9/Gz4ZdyCYUzSSdYUTgqh/p3zO9x3eXtpT7zDxPP8ueEulp
|
||||||
|
hCKLFnhkigALVIRYo7TCb4CgxJJ4uMcIFoaYu4PUxD4jqRk/AMrZiW5nbwscwf1k
|
||||||
|
JuB9UpdHQVWOAXR8TDrrjkjOBYZTOc3ylwSKxUbZV7KvzdDu6oQvcK2+t1gWOAUk
|
||||||
|
/RDN7PbOAIZcoH62IGxYjMvavBWcNxWyzZvABXP0kxTFqPxqgyrQHHmEmTMr0mKk
|
||||||
|
ZEH0LnwJAgMBAAECggEADgjFVqvixlwc36GS7+3Gy/3OWkuuwxis9QrBM6t84L1P
|
||||||
|
ZGG8IONEGleT/PbqUwZvb7Ri9hCx8cWMrjQ83VWviSs3qIoNDrqh3jxDx6cmGojF
|
||||||
|
Fzw3k7R1LYKM7WuCxnZIxvcdGtWs+BilLm+4FZJGAh5dmYPUk2UJx/DNkPEmJx6n
|
||||||
|
4aSEwUnV1QGPR7IjchGEHImBj8T8L3HBQnrsRsCRMwgUs3z0XMgfZoFm9/dq08YA
|
||||||
|
NPzyxAgSsQQrtVOyJMOFj+wi2NrUFtQ0OfS617cUMQXOyceQPpUECn2zPU1UIoPy
|
||||||
|
VgXfkxSXVdraetgNACELJvBI1bvvZctmkJYkEkY5lwKBgQDXcBDCHn4Any+T4gV8
|
||||||
|
Jp9XQf9lCWQnyegP7sSB9vLxzFKU49tuEDDG3m1wOPw17OV/Cs1sOzbiaFXSMM78
|
||||||
|
rD2ljRIODaewr3/Js8B6XbYmmkJqth8ByYuKV6kyq1aFPb4zTmEtcWbnnx45mUHr
|
||||||
|
yZr+8H33yl1iWhR7rdgAtvNP4wKBgQDWkw1C6qR9T8v8sf/vNbwJUHC5xD6B5d18
|
||||||
|
XIV8+Uax1tgmIupy/1hVllLBcaizBpE5eFPsWMMNw2pujnSKNER2hfADSFXYUlMO
|
||||||
|
Ror6e+Dfhr9MctiJjqhNW9kIPBvvnqzpOqwHun6P1GEC/qOi4GIdDDP95OqDaDsV
|
||||||
|
BvKNKVAwIwKBgQCS+iKEvNbLx85mvrFtRNA6cI0zuhd5Sbcnf4bS/845BmNkrpsa
|
||||||
|
WLNeSYsyH755b7gWVyFUcIV+Kx45uxDLsxqPolGqAsjfsqukyRxMnzhQ17buJHe8
|
||||||
|
+WpYpHuLVPc/CaOETznfDdndtWGifBtMKIu02A+oiIfzPG9y/WQ7AJW4bwKBgFmq
|
||||||
|
FW6TEq1yvPEZiLNzJuJVhOV7xgsN/SHMn9N7bzk9aBF3obTwUv9g07AWSMKWyfTT
|
||||||
|
/W3UIZ4MvNr6GGTwNnO4wHT+szC0JhTfEZBeV7fQXPwbObUxsc6xxN2WEK5vBh5n
|
||||||
|
8B9CpUSBIRDZS5PyY81znf5IvF6xHY9J2e13CBU1AoGARfyoIH6gF92bI1DNZ4cT
|
||||||
|
mtgPYEGbWYr28/ADF25CrlZ3HvWDqwt2Oe1EvWWyALLjTyKdZXuc0iFiKUSsgUSm
|
||||||
|
qlBjoh1lWXFrtm+uu8rtGaWrp9q0xjNhcmdM0TqjXpvifDXLALGUiRGvUjZHGo8J
|
||||||
|
mgw1KgJviTZzZu2cy7iFCqg=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { createServer, type Server } from "node:https";
|
||||||
|
import { after, before, test } from "node:test";
|
||||||
|
import { fetchWith } from "../src/lib/http";
|
||||||
|
|
||||||
|
const TLS_OPTIONS = {
|
||||||
|
cert: readFileSync(new URL("./fixtures/selfsigned-cert.pem", import.meta.url)),
|
||||||
|
key: readFileSync(new URL("./fixtures/selfsigned-key.pem", import.meta.url)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let server: Server;
|
||||||
|
let base: string;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
server = createServer(TLS_OPTIONS, (req, res) => {
|
||||||
|
res.writeHead(200, { "content-type": "text/plain" });
|
||||||
|
res.end("ok");
|
||||||
|
});
|
||||||
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||||
|
base = `https://127.0.0.1:${(server.address() as { port: number }).port}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => new Promise((resolve) => server.close(() => resolve())));
|
||||||
|
|
||||||
|
test("fetchWith verifies certificates by default", async () => {
|
||||||
|
await assert.rejects(fetchWith({ baseUrl: base }, `${base}/`));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fetchWith accepts self-signed certificates when insecure", async () => {
|
||||||
|
const res = await fetchWith({ baseUrl: base, insecure: true }, `${base}/`);
|
||||||
|
assert.equal(res.status, 200);
|
||||||
|
assert.equal(await res.text(), "ok");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fetchWith honors init headers and signal when insecure", async () => {
|
||||||
|
let seen: string | undefined;
|
||||||
|
const spy = createServer(TLS_OPTIONS, (req, res) => {
|
||||||
|
seen = req.headers.authorization;
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end("ok");
|
||||||
|
});
|
||||||
|
await new Promise<void>((resolve) => spy.listen(0, "127.0.0.1", resolve));
|
||||||
|
const spyBase = `https://127.0.0.1:${(spy.address() as { port: number }).port}`;
|
||||||
|
try {
|
||||||
|
const res = await fetchWith({ baseUrl: spyBase, insecure: true }, `${spyBase}/`, {
|
||||||
|
headers: { Authorization: "Bearer tok" },
|
||||||
|
signal: AbortSignal.timeout(3000),
|
||||||
|
});
|
||||||
|
assert.equal(res.status, 200);
|
||||||
|
assert.equal(seen, "Bearer tok");
|
||||||
|
} finally {
|
||||||
|
await new Promise<void>((resolve) => spy.close(() => resolve()));
|
||||||
|
}
|
||||||
|
});
|
||||||
+4
-1
@@ -16,10 +16,13 @@ test("cfgFromSettings defaults baseUrl and omits empty apiKey", () => {
|
|||||||
const cfg = cfgFromSettings({});
|
const cfg = cfgFromSettings({});
|
||||||
assert.equal(cfg.baseUrl, "http://localhost:9292");
|
assert.equal(cfg.baseUrl, "http://localhost:9292");
|
||||||
assert.equal(cfg.apiKey, undefined);
|
assert.equal(cfg.apiKey, undefined);
|
||||||
assert.deepEqual(cfgFromSettings({ baseUrl: "http://x/", apiKey: "abc" }), {
|
assert.equal(cfg.insecure, false);
|
||||||
|
assert.deepEqual(cfgFromSettings({ baseUrl: "http://x/", apiKey: "abc", insecure: true }), {
|
||||||
baseUrl: "http://x",
|
baseUrl: "http://x",
|
||||||
apiKey: "abc",
|
apiKey: "abc",
|
||||||
|
insecure: true,
|
||||||
});
|
});
|
||||||
|
assert.equal(cfgFromSettings({ insecure: "yes" }).insecure, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("shorten keeps short names and truncates long ones", () => {
|
test("shorten keeps short names and truncates long ones", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user