@earendil-works/pi-coding-agent
Version:
Coding agent CLI with read, bash, edit, write tools and session management
302 lines • 11.9 kB
JavaScript
function errorMessage(payload, fallback) {
if (typeof payload !== "object" || payload === null)
return fallback;
const error = payload.error;
if (typeof error !== "object" || error === null)
return fallback;
const message = error.message;
return typeof message === "string" && message ? message : fallback;
}
function isModelInfo(value) {
if (typeof value !== "object" || value === null)
return false;
const candidate = value;
return typeof candidate.id === "string" && typeof candidate.status?.value === "string";
}
function linkSignal(source, target) {
if (!source)
return () => { };
if (source.aborted) {
target.abort(source.reason);
return () => { };
}
const abort = () => target.abort(source.reason);
source.addEventListener("abort", abort, { once: true });
return () => source.removeEventListener("abort", abort);
}
function sleep(ms, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
reject(signal.reason ?? new Error("Cancelled"));
return;
}
const abort = () => {
clearTimeout(timeout);
reject(signal?.reason ?? new Error("Cancelled"));
};
const timeout = setTimeout(() => {
signal?.removeEventListener("abort", abort);
resolve();
}, ms);
signal?.addEventListener("abort", abort, { once: true });
});
}
function parseLoadProgress(data) {
if (typeof data !== "object" || data === null)
return undefined;
const progress = data.progress;
if (typeof progress !== "object" || progress === null)
return undefined;
const value = progress;
const stage = typeof value.current === "string" ? value.current : typeof value.stage === "string" ? value.stage : undefined;
const stages = Array.isArray(value.stages)
? value.stages.filter((entry) => typeof entry === "string")
: [];
const stageRatio = typeof value.value === "number" ? Math.max(0, Math.min(1, value.value)) : undefined;
let ratio = stageRatio;
if (stage && stages.length > 0) {
const index = stages.indexOf(stage);
if (index >= 0)
ratio = (index + (stageRatio ?? 0)) / stages.length;
}
return {
message: stage ? `Loading ${stage.replaceAll("_", " ")}` : "Loading model",
ratio,
};
}
function parseDownloadProgress(data) {
if (typeof data !== "object" || data === null)
return undefined;
const nested = data.progress;
const files = typeof nested === "object" && nested !== null ? nested : data;
let done = 0;
let total = 0;
for (const value of Object.values(files)) {
if (typeof value !== "object" || value === null)
continue;
const entry = value;
if (typeof entry.done !== "number" || typeof entry.total !== "number")
continue;
done += entry.done;
total += entry.total;
}
if (total <= 0)
return undefined;
return {
message: "Downloading model",
ratio: done / total,
detail: `${formatBytes(done)} / ${formatBytes(total)}`,
};
}
export function formatBytes(bytes) {
if (bytes < 1024)
return `${bytes} B`;
const units = ["KiB", "MiB", "GiB", "TiB"];
let value = bytes / 1024;
let unit = units[0];
for (let index = 1; index < units.length && value >= 1024; index++) {
value /= 1024;
unit = units[index];
}
return `${value >= 10 ? value.toFixed(1) : value.toFixed(2)} ${unit}`;
}
export function normalizeLlamaServerUrl(value) {
const url = new URL(value.trim());
if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new Error("Server URL must use http or https");
}
url.hash = "";
url.search = "";
url.pathname = url.pathname.replace(/\/+$/u, "").replace(/\/v1$/u, "") || "/";
return url.toString().replace(/\/$/u, "");
}
export function llamaInferenceUrl(serverUrl) {
return `${normalizeLlamaServerUrl(serverUrl)}/v1`;
}
export class LlamaClient {
serverUrl;
apiKey;
constructor(serverUrl, apiKey) {
this.serverUrl = normalizeLlamaServerUrl(serverUrl);
this.apiKey = apiKey;
}
async request(path, init = {}) {
const headers = new Headers(init.headers);
if (init.body !== undefined)
headers.set("Content-Type", "application/json");
if (this.apiKey)
headers.set("Authorization", `Bearer ${this.apiKey}`);
const timeout = AbortSignal.timeout(15_000);
const signal = init.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
const response = await fetch(`${this.serverUrl}${path}`, { ...init, headers, signal });
let payload;
try {
payload = await response.json();
}
catch {
payload = undefined;
}
if (!response.ok)
throw new Error(errorMessage(payload, `llama.cpp returned HTTP ${response.status}`));
return payload;
}
async list(options = {}) {
const payload = await this.request(`/models${options.reload ? "?reload=1" : ""}`, { signal: options.signal });
if (typeof payload !== "object" || payload === null || !Array.isArray(payload.data)) {
throw new Error("llama.cpp returned an invalid model catalog");
}
const data = payload.data;
if (!data.every(isModelInfo))
throw new Error("Server is not running in llama.cpp router mode");
return data;
}
async load(model, signal) {
await this.request("/models/load", { method: "POST", body: JSON.stringify({ model }), signal });
}
async unload(model, signal) {
await this.request("/models/unload", { method: "POST", body: JSON.stringify({ model }), signal });
}
async unloadAndWait(model, signal) {
await this.unload(model, signal);
while (true) {
const entry = (await this.list({ signal })).find((candidate) => candidate.id === model);
if (!entry || entry.status.value === "unloaded")
return;
await sleep(100, signal);
}
}
async download(model, signal) {
await this.request("/models", { method: "POST", body: JSON.stringify({ model }), signal });
}
async watch(onEvent, signal) {
const headers = new Headers();
if (this.apiKey)
headers.set("Authorization", `Bearer ${this.apiKey}`);
const response = await fetch(`${this.serverUrl}/models/sse`, { headers, signal });
if (!response.ok || !response.body)
throw new Error(`llama.cpp SSE returned HTTP ${response.status}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const chunk = await reader.read();
if (chunk.done)
break;
buffer += decoder.decode(chunk.value, { stream: true }).replaceAll("\r\n", "\n");
let boundary = buffer.indexOf("\n\n");
while (boundary >= 0) {
const frame = buffer.slice(0, boundary);
buffer = buffer.slice(boundary + 2);
const data = frame
.split("\n")
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).trimStart())
.join("\n");
if (data) {
try {
const event = JSON.parse(data);
if (event && typeof event.model === "string" && typeof event.event === "string")
onEvent(event);
}
catch {
// Ignore malformed events; catalog polling remains authoritative.
}
}
boundary = buffer.indexOf("\n\n");
}
}
}
async loadAndWait(model, onProgress, signal) {
const watcher = new AbortController();
const unlink = linkSignal(signal, watcher);
let eventLoaded = false;
let eventError;
void this.watch((event) => {
if (event.model !== model)
return;
if (event.event !== "model_status" && event.event !== "status_change")
return;
const data = event.data;
if (data?.status === "loaded")
eventLoaded = true;
if (data?.status === "unloaded")
eventError = "Model failed to load";
const progress = parseLoadProgress(event.data);
if (progress)
onProgress(progress);
}, watcher.signal).catch(() => { });
try {
await this.load(model, signal);
onProgress({ message: "Loading model" });
while (true) {
if (signal?.aborted)
throw signal.reason ?? new Error("Cancelled");
const entry = (await this.list({ signal })).find((candidate) => candidate.id === model);
if (entry?.status.value === "loaded")
return entry;
if (eventLoaded && !entry)
return { id: model, status: { value: "loaded" } };
if (entry?.status.failed || eventError) {
throw new Error(entry?.status.exit_code === undefined
? (eventError ?? "Model failed to load")
: `Model exited with code ${entry.status.exit_code}`);
}
await sleep(250, signal);
}
}
finally {
unlink();
watcher.abort();
}
}
async downloadAndWait(model, onProgress, signal) {
const watcher = new AbortController();
const unlink = linkSignal(signal, watcher);
let finished = false;
let failure;
let sawDownloading = false;
let polls = 0;
void this.watch((event) => {
if (event.model !== model)
return;
if (event.event === "download_finished")
finished = true;
if (event.event === "download_failed")
failure = errorMessage(event.data, "Download failed");
if (event.event === "download_progress") {
sawDownloading = true;
const progress = parseDownloadProgress(event.data);
if (progress)
onProgress(progress);
}
}, watcher.signal).catch(() => { });
try {
await this.download(model, signal);
onProgress({ message: "Downloading model" });
while (true) {
if (signal?.aborted)
throw signal.reason ?? new Error("Cancelled");
if (failure)
throw new Error(failure);
const models = await this.list({ signal });
polls++;
const entry = models.find((candidate) => candidate.id === model);
if (entry?.status.value === "downloading") {
sawDownloading = true;
const progress = parseDownloadProgress(entry.status.progress);
if (progress)
onProgress(progress);
}
else if (finished || (entry && (sawDownloading || polls >= 2))) {
return this.list({ reload: true, signal });
}
await sleep(500, signal);
}
}
finally {
unlink();
watcher.abort();
}
}
}
//# sourceMappingURL=client.js.map