// CRITICALimport type { GPU, Recipe, Status, Config, LifetimeMetrics } from "./types"; const DEFAULT_BASE_URL = "http://localhost:8080"; export class CliApiError extends Error { public readonly status: number | null; public readonly method: string; public readonly path: string; public constructor(message: string, method: string, path: string, status: number | null = null) { super(message); this.name = "CliApiError"; this.status = status; this.method = method; this.path = path; }} function resolveBaseUrl(): string { const configured = process.env.VLLM_STUDIO_URL?.trim() || DEFAULT_BASE_URL; return configured.endsWith("/") ? configured.slice(0, -1) : configured;} function resolveApiKey(): string | undefined { return process.env.VLLM_STUDIO_API_KEY?.trim() || undefined;} function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null;} function toFiniteNumber(value: unknown, fallback = 0): number { if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value === "string") { const parsed = Number(value); if (Number.isFinite(parsed)) return parsed; } return fallback;} function toOptionalFiniteNumber(value: unknown): number | undefined { if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value === "string") { const parsed = Number(value); if (Number.isFinite(parsed)) return parsed; } return undefined;} async function parseBody(response: Response): Promise<unknown> { const text = await response.text(); if (!text) return null; try { return JSON.parse(text); } catch { return text; }} function extractErrorMessage(body: unknown, fallback: string): string { if (typeof body === "string" && body.trim()) return body.trim(); if (isRecord(body)) { const detail = body.detail; if (typeof detail === "string" && detail.trim()) return detail; const error = body.error; if (typeof error === "string" && error.trim()) return error; const message = body.message; if (typeof message === "string" && message.trim()) return message; } return fallback;} async function requestJson<T>( method: "GET" | "POST", path: string, options: { body?: unknown } = {}): Promise<T> { const url = `${resolveBaseUrl()}${path}`; let response: Response; try { response = await fetch(url, { method, headers: { ...(options.body ? { "Content-Type": "application/json" } : {}), ...(resolveApiKey() ? { "X-API-Key": resolveApiKey() } : {}), }, body: options.body ? JSON.stringify(options.body) : undefined, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new CliApiError( `Network error calling ${method} ${path}: ${message}`, method, path, null ); } const body = await parseBody(response); if (!response.ok) { const reason = extractErrorMessage(body, `${response.status} ${response.statusText}`.trim()); throw new CliApiError( `Request failed for ${method} ${path}: ${reason}`, method, path, response.status ); } return body as T;} export async function fetchGPUs(): Promise<GPU[]> { const data = await requestJson<unknown>("GET", "/gpus"); if (!isRecord(data) || !Array.isArray(data.gpus)) { throw new CliApiError("Invalid response for GET /gpus", "GET", "/gpus"); } return data.gpus.filter(isRecord).map((gpu, index) => ({ index: toFiniteNumber(gpu.index, index), name: typeof gpu.name === "string" ? gpu.name : `GPU ${index}`, memory_used: toFiniteNumber(gpu.memory_used), memory_total: toFiniteNumber(gpu.memory_total), utilization: toFiniteNumber(gpu.utilization), temperature: toFiniteNumber(gpu.temperature), power_draw: toFiniteNumber(gpu.power_draw), }));} export async function fetchRecipes(): Promise<Recipe[]> { const data = await requestJson<unknown>("GET", "/recipes"); if (!Array.isArray(data)) { throw new CliApiError("Invalid response for GET /recipes", "GET", "/recipes"); } return data as Recipe[];} export async function fetchStatus(): Promise<Status> { const data = await requestJson<unknown>("GET", "/status"); if (!isRecord(data)) { throw new CliApiError("Invalid response for GET /status", "GET", "/status"); } const processInfo = isRecord(data.process) ? data.process : undefined; return { running: data.running === true, launching: Boolean(data.launching), model: typeof processInfo?.served_model_name === "string" ? processInfo.served_model_name : undefined, backend: typeof processInfo?.backend === "string" ? processInfo.backend : undefined, pid: toOptionalFiniteNumber(processInfo?.pid), port: toOptionalFiniteNumber(processInfo?.port), error: typeof data.error === "string" ? data.error : undefined, };} export async function fetchConfig(): Promise<Config> { const data = await requestJson<unknown>("GET", "/config"); if (!isRecord(data) || !isRecord(data.config)) { throw new CliApiError("Invalid response for GET /config", "GET", "/config"); } const config = data.config; return { port: toFiniteNumber(config.port), inference_port: toFiniteNumber(config.inference_port), models_dir: typeof config.models_dir === "string" ? config.models_dir : "", data_dir: typeof config.data_dir === "string" ? config.data_dir : "", };} export async function fetchLifetimeMetrics(): Promise<LifetimeMetrics> { const data = await requestJson<unknown>("GET", "/lifetime-metrics"); if (!isRecord(data)) { throw new CliApiError("Invalid response for GET /lifetime-metrics", "GET", "/lifetime-metrics"); } return { total_tokens: toFiniteNumber(data.tokens_total), total_requests: toFiniteNumber(data.requests_total), total_energy_kwh: toFiniteNumber(data.energy_kwh), };} export async function launchRecipe(id: string): Promise<boolean> { const data = await requestJson<unknown>("POST", `/launch/${id}`); if (isRecord(data) && typeof data.success === "boolean") return data.success; return true;} export async function evictModel(): Promise<boolean> { const data = await requestJson<unknown>("POST", "/evict"); if (isRecord(data) && typeof data.success === "boolean") return data.success; return true;}
// CRITICAL
import type { GPU, Recipe, Status, Config, LifetimeMetrics } from "./types";
const DEFAULT_BASE_URL = "http://localhost:8080";
export class CliApiError extends Error {
public readonly status: number | null;
public readonly method: string;
public readonly path: string;
public constructor(message: string, method: string, path: string, status: number | null = null) {
super(message);
this.name = "CliApiError";
this.status = status;
this.method = method;
this.path = path;
}
}
function resolveBaseUrl(): string {
const configured = process.env.VLLM_STUDIO_URL?.trim() || DEFAULT_BASE_URL;
return configured.endsWith("/") ? configured.slice(0, -1) : configured;
}
function resolveApiKey(): string | undefined {
return process.env.VLLM_STUDIO_API_KEY?.trim() || undefined;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function toFiniteNumber(value: unknown, fallback = 0): number {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string") {
const parsed = Number(value);
if (Number.isFinite(parsed)) return parsed;
}
return fallback;
}
function toOptionalFiniteNumber(value: unknown): number | undefined {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string") {
const parsed = Number(value);
if (Number.isFinite(parsed)) return parsed;
}
return undefined;
}
async function parseBody(response: Response): Promise<unknown> {
const text = await response.text();
if (!text) return null;
try {
return JSON.parse(text);
} catch {
return text;
}
}
function extractErrorMessage(body: unknown, fallback: string): string {
if (typeof body === "string" && body.trim()) return body.trim();
if (isRecord(body)) {
const detail = body.detail;
if (typeof detail === "string" && detail.trim()) return detail;
const error = body.error;
if (typeof error === "string" && error.trim()) return error;
const message = body.message;
if (typeof message === "string" && message.trim()) return message;
}
return fallback;
}
async function requestJson<T>(
method: "GET" | "POST",
path: string,
options: { body?: unknown } = {}
): Promise<T> {
const url = `${resolveBaseUrl()}${path}`;
let response: Response;
try {
response = await fetch(url, {
method,
headers: {
...(options.body ? { "Content-Type": "application/json" } : {}),
...(resolveApiKey() ? { "X-API-Key": resolveApiKey() } : {}),
},
body: options.body ? JSON.stringify(options.body) : undefined,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new CliApiError(
`Network error calling ${method} ${path}: ${message}`,
method,
path,
null
);
}
const body = await parseBody(response);
if (!response.ok) {
const reason = extractErrorMessage(body, `${response.status} ${response.statusText}`.trim());
throw new CliApiError(
`Request failed for ${method} ${path}: ${reason}`,
method,
path,
response.status
);
}
return body as T;
}
export async function fetchGPUs(): Promise<GPU[]> {
const data = await requestJson<unknown>("GET", "/gpus");
if (!isRecord(data) || !Array.isArray(data.gpus)) {
throw new CliApiError("Invalid response for GET /gpus", "GET", "/gpus");
}
return data.gpus.filter(isRecord).map((gpu, index) => ({
index: toFiniteNumber(gpu.index, index),
name: typeof gpu.name === "string" ? gpu.name : `GPU ${index}`,
memory_used: toFiniteNumber(gpu.memory_used),
memory_total: toFiniteNumber(gpu.memory_total),
utilization: toFiniteNumber(gpu.utilization),
temperature: toFiniteNumber(gpu.temperature),
power_draw: toFiniteNumber(gpu.power_draw),
}));
}
export async function fetchRecipes(): Promise<Recipe[]> {
const data = await requestJson<unknown>("GET", "/recipes");
if (!Array.isArray(data)) {
throw new CliApiError("Invalid response for GET /recipes", "GET", "/recipes");
}
return data as Recipe[];
}
export async function fetchStatus(): Promise<Status> {
const data = await requestJson<unknown>("GET", "/status");
if (!isRecord(data)) {
throw new CliApiError("Invalid response for GET /status", "GET", "/status");
}
const processInfo = isRecord(data.process) ? data.process : undefined;
return {
running: data.running === true,
launching: Boolean(data.launching),
model:
typeof processInfo?.served_model_name === "string"
? processInfo.served_model_name
: undefined,
backend: typeof processInfo?.backend === "string" ? processInfo.backend : undefined,
pid: toOptionalFiniteNumber(processInfo?.pid),
port: toOptionalFiniteNumber(processInfo?.port),
error: typeof data.error === "string" ? data.error : undefined,
};
}
export async function fetchConfig(): Promise<Config> {
const data = await requestJson<unknown>("GET", "/config");
if (!isRecord(data) || !isRecord(data.config)) {
throw new CliApiError("Invalid response for GET /config", "GET", "/config");
}
const config = data.config;
return {
port: toFiniteNumber(config.port),
inference_port: toFiniteNumber(config.inference_port),
models_dir: typeof config.models_dir === "string" ? config.models_dir : "",
data_dir: typeof config.data_dir === "string" ? config.data_dir : "",
};
}
export async function fetchLifetimeMetrics(): Promise<LifetimeMetrics> {
const data = await requestJson<unknown>("GET", "/lifetime-metrics");
if (!isRecord(data)) {
throw new CliApiError("Invalid response for GET /lifetime-metrics", "GET", "/lifetime-metrics");
}
return {
total_tokens: toFiniteNumber(data.tokens_total),
total_requests: toFiniteNumber(data.requests_total),
total_energy_kwh: toFiniteNumber(data.energy_kwh),
};
}
export async function launchRecipe(id: string): Promise<boolean> {
const data = await requestJson<unknown>("POST", `/launch/${id}`);
if (isRecord(data) && typeof data.success === "boolean") return data.success;
return true;
}
export async function evictModel(): Promise<boolean> {
const data = await requestJson<unknown>("POST", "/evict");
if (isRecord(data) && typeof data.success === "boolean") return data.success;
return true;
}