fix: support multiple keys per action and harden runtime/parsing

This commit is contained in:
2026-08-14 10:59:25 -06:00
parent 7e3377f932
commit 733bc2b4f9
10 changed files with 180 additions and 87 deletions
+29 -26
View File
@@ -7,36 +7,39 @@ type DataSourceItem = { label: string; value: string };
export function registerDataSources(): void {
streamDeck.ui.onSendToPlugin(async (ev) => {
const request = ev.payload as { event?: string } | undefined;
const event = request?.event;
if (!event) return;
const settings = await ev.action.getSettings<CfgSettings>();
try {
const request = ev.payload as { event?: string } | undefined;
const event = request?.event;
if (!event) return;
const settings = await ev.action.getSettings<CfgSettings>();
if (event === "models") {
let items: DataSourceItem[] = [];
try {
const models = await fetchModels(cfgFromSettings(settings));
items = models.map((m) => ({ label: `${m.id} (${m.status})`, value: m.id }));
} catch {
items = [];
if (event === "models") {
let items: DataSourceItem[] = [];
try {
const models = await fetchModels(cfgFromSettings(settings));
items = models.map((m) => ({ label: `${m.id} (${m.status})`, value: m.id }));
} catch {
items = [];
}
await streamDeck.ui.sendToPropertyInspector({ event, items });
return;
}
await streamDeck.ui.sendToPropertyInspector({ event, items });
return;
}
if (event === "gpus") {
let items: DataSourceItem[] = [];
try {
const text = await fetchMetrics(cfgFromSettings(settings));
const gpus = gpuInfos(parseGpuMetrics(text));
items = [
{ label: "All GPUs", value: "all" },
...gpus.map((g) => ({ label: `GPU ${g.id} · ${g.name}`, value: g.id })),
];
} catch {
items = [{ label: "All GPUs", value: "all" }];
if (event === "gpus") {
let items: DataSourceItem[] = [];
try {
const text = await fetchMetrics(cfgFromSettings(settings));
const gpus = gpuInfos(parseGpuMetrics(text));
items = [
{ label: "All GPUs", value: "all" },
...gpus.map((g) => ({ label: `GPU ${g.id} · ${g.name}`, value: g.id })),
];
} catch {
items = [{ label: "All GPUs", value: "all" }];
}
await streamDeck.ui.sendToPropertyInspector({ event, items });
}
await streamDeck.ui.sendToPropertyInspector({ event, items });
} catch {
}
});
}
+8 -2
View File
@@ -9,8 +9,13 @@ export function decodeEvent(msg: SseMessage): FeedEvent | null {
const inner = JSON.parse(outer.data) as Record<string, unknown>;
if (outer.type === "inflight") {
const requests: InflightRequest[] =
(inner.requests as InflightRequest[]) ?? (inner.request ? [inner.request as InflightRequest] : []);
const requests: InflightRequest[] = Array.isArray(inner.requests)
? (inner.requests as InflightRequest[])
: Array.isArray(inner.request)
? (inner.request as InflightRequest[])
: inner.request && typeof inner.request === "object"
? [inner.request as InflightRequest]
: [];
return {
type: "inflight",
operation: inner.operation as "snapshot" | "add" | "remove",
@@ -19,6 +24,7 @@ export function decodeEvent(msg: SseMessage): FeedEvent | null {
}
if (outer.type === "modelStatus") {
if (!Array.isArray(inner)) return null;
return { type: "modelStatus", models: inner as unknown as ModelState[] };
}
+6 -1
View File
@@ -1,6 +1,6 @@
import { fetchMetrics } from "./llamaswap";
import { type LlamaSwapConfig } from "./util";
import { gpuInfos, parseGpuMetrics, type GpuMetricKind, type GpuSample } from "./metrics-parser";
import { parseGpuMetrics, type GpuMetricKind, type GpuSample } from "./metrics-parser";
const RING_SIZE = 60;
@@ -34,6 +34,7 @@ export class MetricsPoller {
private gpuIds: string[] = [];
private listeners = new Set<() => void>();
private timer?: ReturnType<typeof setInterval>;
private ticking = false;
private lastError?: string;
constructor(
@@ -43,6 +44,7 @@ export class MetricsPoller {
) {}
start(): void {
if (this.timer) return;
void this.tick();
this.timer = setInterval(() => void this.tick(), this.intervalMs);
}
@@ -60,6 +62,8 @@ export class MetricsPoller {
}
async tick(): Promise<void> {
if (this.ticking) return;
this.ticking = true;
try {
const text = await this.fetchFn(this.cfg);
this.apply(parseGpuMetrics(text));
@@ -67,6 +71,7 @@ export class MetricsPoller {
} catch (err) {
this.lastError = err instanceof Error ? err.message : String(err);
} finally {
this.ticking = false;
for (const listener of this.listeners) listener();
}
}
+9
View File
@@ -6,11 +6,20 @@ import { type LlamaSwapConfig } from "./util";
class Runtime {
readonly tracker = new InflightTracker();
offline = true;
private cfg?: LlamaSwapConfig;
private feed?: EventFeed;
private poller?: MetricsPoller;
private listeners = new Set<() => void>();
ensureConnections(cfg: LlamaSwapConfig): void {
const changed = !this.cfg || this.cfg.baseUrl !== cfg.baseUrl || this.cfg.apiKey !== cfg.apiKey;
if (changed) {
this.feed?.stop();
this.poller?.stop();
this.feed = undefined;
this.poller = undefined;
this.cfg = cfg;
}
if (!this.feed) {
this.feed = new EventFeed(cfg, (ev) => {
this.tracker.apply(ev);
+1
View File
@@ -4,6 +4,7 @@ export interface SseMessage {
}
export function parseSse(chunk: string): { messages: SseMessage[]; rest: string } {
chunk = chunk.replace(/\r\n/g, "\n");
const messages: SseMessage[] = [];
let rest = chunk;
while (true) {