chore(vendor): import pi 0.85.1

Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
This commit is contained in:
2026-09-14 10:48:47 -04:00
parent 5ebc7018c3
commit 4def357d09
1669 changed files with 397627 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
import type { InlineExtension } from "../core/extensions/types.ts";
import llamaExtension from "./llama/index.ts";
export const builtInExtensions: InlineExtension[] = [{ name: "llama.cpp", factory: llamaExtension, hidden: true }];

View File

@@ -0,0 +1,343 @@
export type LlamaModelStatus = "unloaded" | "loading" | "loaded" | "downloading" | "sleeping";
export interface LlamaModelInfo {
id: string;
aliases?: string[];
status: {
value: LlamaModelStatus;
args?: string[];
failed?: boolean;
exit_code?: number;
progress?: Record<string, { done: number; total: number }>;
};
architecture?: {
input_modalities?: string[];
output_modalities?: string[];
};
source?: string;
meta?: {
n_ctx?: number;
n_ctx_train?: number;
size?: number;
ftype?: string;
};
}
export interface LlamaModelsResponse {
data: LlamaModelInfo[];
object?: string;
}
export interface LlamaServerProps {
models_autoload?: boolean;
}
export interface LlamaModelEvent {
model: string;
event: string;
data?: unknown;
}
export interface LlamaProgress {
message: string;
ratio?: number;
detail?: string;
}
function errorMessage(payload: unknown, fallback: string): string {
if (typeof payload !== "object" || payload === null) return fallback;
const error = (payload as { error?: unknown }).error;
if (typeof error !== "object" || error === null) return fallback;
const message = (error as { message?: unknown }).message;
return typeof message === "string" && message ? message : fallback;
}
function isModelInfo(value: unknown): value is LlamaModelInfo {
if (typeof value !== "object" || value === null) return false;
const candidate = value as { id?: unknown; status?: { value?: unknown } };
return typeof candidate.id === "string" && typeof candidate.status?.value === "string";
}
function linkSignal(source: AbortSignal | undefined, target: AbortController): () => void {
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: number, signal?: AbortSignal): Promise<void> {
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: unknown): LlamaProgress | undefined {
if (typeof data !== "object" || data === null) return undefined;
const progress = (data as { progress?: unknown }).progress;
if (typeof progress !== "object" || progress === null) return undefined;
const value = progress as { stages?: unknown; current?: unknown; stage?: unknown; value?: unknown };
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): entry is string => 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: unknown): LlamaProgress | undefined {
if (typeof data !== "object" || data === null) return undefined;
const nested = (data as { progress?: unknown }).progress;
const files = typeof nested === "object" && nested !== null ? nested : data;
let done = 0;
let total = 0;
for (const value of Object.values(files as Record<string, unknown>)) {
if (typeof value !== "object" || value === null) continue;
const entry = value as { done?: unknown; total?: unknown };
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: number): string {
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: string): string {
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: string): string {
return `${normalizeLlamaServerUrl(serverUrl)}/v1`;
}
export class LlamaClient {
readonly serverUrl: string;
private readonly apiKey: string | undefined;
constructor(serverUrl: string, apiKey?: string) {
this.serverUrl = normalizeLlamaServerUrl(serverUrl);
this.apiKey = apiKey;
}
private async request(path: string, init: RequestInit = {}): Promise<unknown> {
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: unknown;
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: { reload?: boolean; signal?: AbortSignal } = {}): Promise<LlamaModelInfo[]> {
const payload = await this.request(`/models${options.reload ? "?reload=1" : ""}`, { signal: options.signal });
if (typeof payload !== "object" || payload === null || !Array.isArray((payload as { data?: unknown }).data)) {
throw new Error("llama.cpp returned an invalid model catalog");
}
const data = (payload as { data: unknown[] }).data;
if (!data.every(isModelInfo)) throw new Error("Server is not running in llama.cpp router mode");
return data;
}
async props(options: { signal?: AbortSignal } = {}): Promise<LlamaServerProps> {
const payload = await this.request("/props", { signal: options.signal });
if (typeof payload !== "object" || payload === null) return {};
const { models_autoload: modelsAutoload } = payload as Record<string, unknown>;
return typeof modelsAutoload === "boolean" ? { models_autoload: modelsAutoload } : {};
}
async load(model: string, signal?: AbortSignal): Promise<void> {
await this.request("/models/load", { method: "POST", body: JSON.stringify({ model }), signal });
}
async unload(model: string, signal?: AbortSignal): Promise<void> {
await this.request("/models/unload", { method: "POST", body: JSON.stringify({ model }), signal });
}
async unloadAndWait(model: string, signal?: AbortSignal): Promise<void> {
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: string, signal?: AbortSignal): Promise<void> {
await this.request("/models", { method: "POST", body: JSON.stringify({ model }), signal });
}
async watch(onEvent: (event: LlamaModelEvent) => void, signal?: AbortSignal): Promise<void> {
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) as LlamaModelEvent;
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: string,
onProgress: (progress: LlamaProgress) => void,
signal?: AbortSignal,
): Promise<LlamaModelInfo> {
const watcher = new AbortController();
const unlink = linkSignal(signal, watcher);
let eventLoaded = false;
let eventError: string | undefined;
void this.watch((event) => {
if (event.model !== model) return;
if (event.event !== "model_status" && event.event !== "status_change") return;
const data = event.data as { status?: unknown } | undefined;
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: string,
onProgress: (progress: LlamaProgress) => void,
signal?: AbortSignal,
): Promise<LlamaModelInfo[]> {
const watcher = new AbortController();
const unlink = linkSignal(signal, watcher);
let finished = false;
let failure: string | undefined;
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();
}
}
}

View File

@@ -0,0 +1,158 @@
import { readFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
const DEFAULT_HUGGING_FACE_URL = "https://huggingface.co";
const QUANTIZATION_PATTERN =
/(?:^|[-_.])((?:UD-)?(?:IQ\d(?:_[A-Z0-9]+)+|Q\d(?:_[A-Z0-9]+)+|BF16|F16|F32|MXFP\d(?:_[A-Z0-9]+)*))$/iu;
const SHARD_SUFFIX_PATTERN = /-\d{5}-of-\d{5}$/u;
export interface HuggingFaceModel {
id: string;
downloads: number;
}
export interface HuggingFaceQuantization {
name: string;
size?: number;
}
export interface HuggingFaceModelDetails {
id: string;
gated: false | "auto" | "manual";
quantizations: HuggingFaceQuantization[];
}
function payloadError(payload: unknown, fallback: string): string {
if (typeof payload !== "object" || payload === null) return fallback;
const error = (payload as { error?: unknown }).error;
return typeof error === "string" && error ? error : fallback;
}
function parseRateLimitDelay(value: string | null): number | undefined {
const match = value?.match(/(?:^|;)t=(\d+)/u);
return match ? Number(match[1]) : undefined;
}
async function readToken(path: string): Promise<string | undefined> {
try {
const token = (await readFile(path, "utf8")).trim();
return token || undefined;
} catch {
return undefined;
}
}
export async function findHuggingFaceToken(env: NodeJS.ProcessEnv = process.env): Promise<string | undefined> {
const fromEnvironment = env.HF_TOKEN?.trim();
if (fromEnvironment) return fromEnvironment;
const paths = [
env.HF_TOKEN_PATH,
env.HF_HOME ? join(env.HF_HOME, "token") : undefined,
env.XDG_CACHE_HOME ? join(env.XDG_CACHE_HOME, "huggingface", "token") : undefined,
join(homedir(), ".cache", "huggingface", "token"),
].filter((path): path is string => Boolean(path));
for (const path of new Set(paths)) {
const token = await readToken(path);
if (token) return token;
}
return undefined;
}
export class HuggingFaceClient {
private readonly token: string | undefined;
private readonly baseUrl: string;
constructor(token?: string, baseUrl = DEFAULT_HUGGING_FACE_URL) {
this.token = token;
this.baseUrl = baseUrl.replace(/\/+$/u, "");
}
private async request(path: string, signal?: AbortSignal): Promise<unknown> {
const headers = new Headers();
if (this.token) headers.set("Authorization", `Bearer ${this.token}`);
const timeout = AbortSignal.timeout(15_000);
const response = await fetch(`${this.baseUrl}${path}`, {
headers,
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
});
let payload: unknown;
try {
payload = await response.json();
} catch {
payload = undefined;
}
if (!response.ok) {
const fallback = `Hugging Face returned HTTP ${response.status}`;
if (response.status === 429) {
const delay =
Number(response.headers.get("retry-after")) || parseRateLimitDelay(response.headers.get("ratelimit"));
throw new Error(
delay ? `Hugging Face rate limit reached; retry in ${delay}s` : "Hugging Face rate limit reached",
);
}
throw new Error(payloadError(payload, fallback));
}
return payload;
}
async search(query: string, signal?: AbortSignal): Promise<HuggingFaceModel[]> {
const params = new URLSearchParams({
search: query,
filter: "gguf",
sort: "downloads",
direction: "-1",
limit: "20",
});
const payload = await this.request(`/api/models?${params}`, signal);
if (!Array.isArray(payload)) throw new Error("Hugging Face returned invalid search results");
return payload.flatMap((value) => {
if (typeof value !== "object" || value === null || typeof (value as { id?: unknown }).id !== "string")
return [];
const model = value as { id: string; downloads?: unknown };
return [{ id: model.id, downloads: typeof model.downloads === "number" ? model.downloads : 0 }];
});
}
async details(id: string, signal?: AbortSignal): Promise<HuggingFaceModelDetails> {
const encodedId = id.split("/").map(encodeURIComponent).join("/");
const payload = await this.request(`/api/models/${encodedId}?blobs=true`, signal);
if (typeof payload !== "object" || payload === null) {
throw new Error("Hugging Face returned invalid model details");
}
const model = payload as { id?: unknown; gated?: unknown; siblings?: unknown };
const sizes = new Map<string, { total: number; complete: boolean }>();
if (Array.isArray(model.siblings)) {
for (const value of model.siblings) {
if (typeof value !== "object" || value === null) continue;
const file = value as { rfilename?: unknown; size?: unknown };
if (typeof file.rfilename !== "string" || !file.rfilename.toLowerCase().endsWith(".gguf")) continue;
const filename = file.rfilename.split("/").at(-1)!;
if (filename.toLowerCase().startsWith("mmproj")) continue;
const stem = filename.slice(0, -5).replace(SHARD_SUFFIX_PATTERN, "");
const quantization = stem.match(QUANTIZATION_PATTERN)?.[1]?.toUpperCase();
if (!quantization) continue;
const current = sizes.get(quantization) ?? { total: 0, complete: true };
if (typeof file.size === "number") current.total += file.size;
else current.complete = false;
sizes.set(quantization, current);
}
}
const quantizations = [...sizes]
.map(([name, size]) => ({ name, size: size.complete ? size.total : undefined }))
.sort((left, right) => {
if (left.name === "Q4_K_M") return -1;
if (right.name === "Q4_K_M") return 1;
return (
(left.size ?? Number.MAX_SAFE_INTEGER) - (right.size ?? Number.MAX_SAFE_INTEGER) ||
left.name.localeCompare(right.name)
);
});
return {
id: typeof model.id === "string" ? model.id : id,
gated: model.gated === "auto" || model.gated === "manual" ? model.gated : false,
quantizations,
};
}
}

View File

@@ -0,0 +1,230 @@
import type { ExtensionAPI, ExtensionCommandContext } from "../../core/extensions/types.ts";
import { formatBytes, LlamaClient, type LlamaModelInfo, normalizeLlamaServerUrl } from "./client.ts";
import { findHuggingFaceToken, HuggingFaceClient } from "./huggingface.ts";
import { createLlamaProvider, LLAMA_PROVIDER_ID } from "./provider.ts";
import { type LlamaUi, runWithProgress, showLlamaUi } from "./ui.ts";
function modelIsLoaded(model: LlamaModelInfo): boolean {
return model.status.value === "loaded" || model.status.value === "sleeping";
}
function isConnectionError(error: unknown): boolean {
if (!(error instanceof Error)) return false;
const message = `${error.name} ${error.message}`.toLowerCase();
return message.includes("fetch failed") || message.includes("timeout") || message.includes("network");
}
function connectionErrorMessage(error: unknown): string {
if (isConnectionError(error)) return "Could not connect to the server.";
return error instanceof Error ? error.message : String(error);
}
function parseHuggingFaceModel(value: string): { repository: string; quantization?: string } {
const colon = value.indexOf(":", value.indexOf("/") + 1);
return colon < 0
? { repository: value }
: { repository: value.slice(0, colon), quantization: value.slice(colon + 1) };
}
async function configuredClient(ctx: ExtensionCommandContext): Promise<LlamaClient | undefined> {
const result = await ctx.modelRegistry.getProviderAuth(LLAMA_PROVIDER_ID);
if (!result) {
ctx.ui.notify(`Configure llama.cpp with /login ${LLAMA_PROVIDER_ID}`, "warning");
return undefined;
}
const configuredUrl = result.env?.LLAMA_BASE_URL;
const serverUrl = normalizeLlamaServerUrl(
typeof configuredUrl === "string" && configuredUrl ? configuredUrl : (result.auth.baseUrl ?? ""),
);
return new LlamaClient(serverUrl, result.auth.apiKey);
}
export default function llamaExtension(pi: ExtensionAPI): void {
const provider = createLlamaProvider();
pi.registerProvider(provider.provider);
const syncCatalog = async (
ctx: ExtensionCommandContext,
client: LlamaClient,
catalog?: LlamaModelInfo[],
): Promise<LlamaModelInfo[]> => {
const signal = AbortSignal.timeout(15_000);
const current = catalog ?? (await client.list({ signal }));
provider.setCatalog(current, client.serverUrl);
const result = await ctx.modelRegistry.refresh({
providers: [LLAMA_PROVIDER_ID],
// /llama already contacted the configured llama.cpp server, so keep this refresh live even in PI_OFFLINE.
allowNetwork: true,
signal,
});
if (result.aborted) throw new Error("Model catalog refresh timed out.");
const refreshError = result.errors.get(LLAMA_PROVIDER_ID);
if (refreshError) throw refreshError;
return current;
};
const loadModel = async (
ctx: ExtensionCommandContext,
ui: LlamaUi,
client: LlamaClient,
catalog: LlamaModelInfo[],
target: LlamaModelInfo,
): Promise<void> => {
const loaded = catalog.filter((model) => model.id !== target.id && modelIsLoaded(model));
let replace = false;
if (loaded.length > 0) {
const choice = await ui.select(`${loaded.length} model${loaded.length === 1 ? " is" : "s are"} loaded`, [
"Unload all and load",
"Keep loaded and load",
"Cancel",
]);
if (!choice || choice === "Cancel") return;
replace = choice === "Unload all and load";
}
const restoreLoaded = async (): Promise<void> => {
ctx.ui.notify("Restoring previously loaded models");
for (const model of loaded) await client.loadAndWait(model.id, () => {});
await syncCatalog(ctx, client);
};
if (replace) {
for (const model of loaded) await client.unloadAndWait(model.id);
}
try {
const result = await runWithProgress(ui, {
title: "Loading model",
model: target.id,
initialMessage: "Starting…",
cancelTitle: "Stop loading?",
cancelMessage: target.id,
run: (signal, update) => client.loadAndWait(target.id, update, signal),
cancel: () => client.unload(target.id),
});
if (result.cancelled) {
if (replace) await restoreLoaded();
return;
}
const refreshed = await syncCatalog(ctx, client);
const loadedModel = refreshed.find((model) => model.id === target.id);
ctx.ui.notify(
loadedModel?.status.value === "loaded" ? `Loaded ${target.id}` : `Load started for ${target.id}`,
);
} catch (error) {
if (replace) {
try {
await restoreLoaded();
} catch {
// Preserve the original load error.
}
}
throw error;
}
};
const unloadModel = async (
ctx: ExtensionCommandContext,
ui: LlamaUi,
client: LlamaClient,
model: LlamaModelInfo,
): Promise<void> => {
if (!(await ui.confirm("Unload model?", model.id))) return;
await client.unloadAndWait(model.id);
await syncCatalog(ctx, client);
ctx.ui.notify(`Unloaded ${model.id}`);
};
const downloadModel = async (ctx: ExtensionCommandContext, ui: LlamaUi, client: LlamaClient): Promise<void> => {
const huggingFace = new HuggingFaceClient(await findHuggingFaceToken());
const selected = await ui.searchModels((query, signal) => huggingFace.search(query, signal));
if (!selected) return;
const parsed = parseHuggingFaceModel(selected);
ui.showStatus("Loading model details", parsed.repository);
const details = await huggingFace.details(parsed.repository);
if (details.gated) {
const approval = details.gated === "manual" ? "Manual approval is required" : "Accept the access terms";
const choice = await ui.select(
`Hugging Face access required\n${details.id}\n\n${approval} at:\nhttps://huggingface.co/${details.id}\n\nThe llama.cpp server needs HF_TOKEN with access.`,
["Continue", "Back"],
);
if (choice !== "Continue") return;
}
let quantization = parsed.quantization;
if (!quantization && details.quantizations.length > 0) {
const options = details.quantizations.map((entry) => {
const detail = [
entry.size === undefined ? undefined : formatBytes(entry.size),
entry.name === "Q4_K_M" ? "recommended" : undefined,
]
.filter((value): value is string => Boolean(value))
.join(" · ");
return detail ? `${entry.name} · ${detail}` : entry.name;
});
const choice = await ui.select(`Select quantization\n${details.id}`, options);
if (!choice) return;
quantization = details.quantizations[options.indexOf(choice)]?.name;
if (!quantization) return;
}
const model = quantization ? `${details.id}:${quantization}` : details.id;
const result = await runWithProgress(ui, {
title: "Downloading model",
model,
initialMessage: "Starting…",
cancelTitle: "Stop download?",
cancelMessage: model,
run: (signal, update) => client.downloadAndWait(model, update, signal),
cancel: () => client.unload(model),
});
if (result.cancelled) return;
await syncCatalog(ctx, client, result.value);
ctx.ui.notify(`Downloaded ${model}`);
};
pi.registerCommand("llama", {
description: "Manage llama.cpp router models",
handler: async (_args, ctx) => {
if (ctx.mode !== "tui") {
ctx.ui.notify("/llama is available in interactive mode", "warning");
return;
}
const client = await configuredClient(ctx);
if (!client) return;
await showLlamaUi(ctx, async (ui) => {
const readCatalog = async (): Promise<LlamaModelInfo[] | undefined> => {
while (true) {
try {
return await syncCatalog(ctx, client);
} catch (error) {
if ((await ui.connectionError(client.serverUrl, connectionErrorMessage(error))) === "close") {
return undefined;
}
}
}
};
let catalog = await readCatalog();
if (!catalog) return;
while (true) {
const action = await ui.showModels(client.serverUrl, catalog);
if (action.type === "close") return;
let actionError: unknown;
try {
if (action.type === "download") await downloadModel(ctx, ui, client);
else if (modelIsLoaded(action.model)) await unloadModel(ctx, ui, client, action.model);
else if (action.model.status.value === "unloaded")
await loadModel(ctx, ui, client, catalog, action.model);
else ctx.ui.notify(`${action.model.id} is ${action.model.status.value}`, "warning");
} catch (error) {
actionError = error;
}
const refreshed = await readCatalog();
if (!refreshed) return;
catalog = refreshed;
if (actionError && !isConnectionError(actionError)) {
ctx.ui.notify(actionError instanceof Error ? actionError.message : String(actionError), "error");
}
}
});
},
});
}

View File

@@ -0,0 +1,180 @@
import type {
ApiKeyCredential,
AuthContext,
AuthResult,
Model,
Provider,
ProviderStreamOptions,
RefreshModelsContext,
} from "@earendil-works/pi-ai";
import { stream, streamSimple } from "@earendil-works/pi-ai/compat";
import { LlamaClient, type LlamaModelInfo, llamaInferenceUrl, normalizeLlamaServerUrl } from "./client.ts";
export const LLAMA_PROVIDER_ID = "llama.cpp";
export const DEFAULT_LLAMA_SERVER_URL = "http://127.0.0.1:8080";
function credentialServerUrl(credential: ApiKeyCredential | undefined): string | undefined {
const value = credential?.env?.LLAMA_BASE_URL;
return typeof value === "string" && value.trim() ? normalizeLlamaServerUrl(value) : undefined;
}
async function resolveServerUrl(
ctx: AuthContext,
credential: ApiKeyCredential | undefined,
): Promise<string | undefined> {
const configured = credentialServerUrl(credential) ?? (await ctx.env("LLAMA_BASE_URL"))?.trim();
return configured ? normalizeLlamaServerUrl(configured) : undefined;
}
function modelIsSelectable(model: LlamaModelInfo, routerAutoload: boolean): boolean {
if (model.status.value === "loaded") return true;
// llama.cpp reports idle-slept models as "sleeping"; requests wake them automatically.
if (model.status.value === "sleeping") return true;
// Unloaded presets are routable only when llama.cpp router autoload can load them on first use.
return routerAutoload && model.status.value === "unloaded" && !model.status.failed && model.source === "preset";
}
async function routerAutoloadEnabled(
client: LlamaClient,
catalog: readonly LlamaModelInfo[],
signal: AbortSignal,
): Promise<boolean> {
if (!catalog.some((model) => model.status.value === "unloaded" && model.source === "preset")) return false;
try {
return (await client.props({ signal })).models_autoload === true;
} catch {
return false;
}
}
function toPiModel(model: LlamaModelInfo, serverUrl: string): Model<"openai-completions"> {
const reportedContextWindow = model.meta?.n_ctx ?? model.meta?.n_ctx_train;
const contextWindow = reportedContextWindow && reportedContextWindow > 0 ? reportedContextWindow : 128000;
return {
id: model.id,
name: model.id,
api: "openai-completions",
provider: LLAMA_PROVIDER_ID,
baseUrl: llamaInferenceUrl(serverUrl),
reasoning: false,
input: model.architecture?.input_modalities?.includes("image") ? ["text", "image"] : ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow,
maxTokens: contextWindow,
compat: {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
supportsUsageInStreaming: true,
supportsStrictMode: false,
maxTokensField: "max_tokens",
},
};
}
export interface LlamaProviderController {
provider: Provider<"openai-completions">;
setCatalog(models: readonly LlamaModelInfo[], serverUrl: string, options?: { routerAutoload?: boolean }): void;
}
export function createLlamaProvider(): LlamaProviderController {
let models: readonly Model<"openai-completions">[] = [];
const setCatalog = (
catalog: readonly LlamaModelInfo[],
serverUrl: string,
options: { routerAutoload?: boolean } = {},
): void => {
models = catalog
.filter((model) => modelIsSelectable(model, options.routerAutoload === true))
.map((model) => toPiModel(model, serverUrl));
};
const provider: Provider<"openai-completions"> = {
id: LLAMA_PROVIDER_ID,
name: "llama.cpp",
baseUrl: llamaInferenceUrl(DEFAULT_LLAMA_SERVER_URL),
auth: {
apiKey: {
name: "llama.cpp server",
login: async (interaction): Promise<ApiKeyCredential> => {
const enteredUrl = await interaction.prompt({
type: "text",
message: "llama.cpp server URL",
placeholder: process.env.LLAMA_BASE_URL ?? DEFAULT_LLAMA_SERVER_URL,
});
const serverUrl = normalizeLlamaServerUrl(
enteredUrl.trim() || process.env.LLAMA_BASE_URL || DEFAULT_LLAMA_SERVER_URL,
);
const apiKey = (
await interaction.prompt({
type: "secret",
message: "API key (optional)",
})
).trim();
await new LlamaClient(serverUrl, apiKey || undefined).list({ signal: interaction.signal });
return {
type: "api_key",
key: apiKey || undefined,
env: { LLAMA_BASE_URL: serverUrl },
};
},
check: async ({ ctx, credential }) => {
const serverUrl = await resolveServerUrl(ctx, credential);
return serverUrl
? { type: "api_key", source: credential ? "stored credential" : "LLAMA_BASE_URL" }
: undefined;
},
resolve: async ({ ctx, credential }): Promise<AuthResult | undefined> => {
const serverUrl = await resolveServerUrl(ctx, credential);
if (!serverUrl) return undefined;
const apiKey = credential?.key ?? (await ctx.env("LLAMA_API_KEY")) ?? "local";
return {
auth: { apiKey, baseUrl: llamaInferenceUrl(serverUrl) },
env: { ...credential?.env, LLAMA_BASE_URL: serverUrl },
source: credential ? "stored credential" : "LLAMA_BASE_URL",
};
},
},
},
getModels: () => models,
refreshModels: async (context: RefreshModelsContext): Promise<void> => {
if (context.stored) {
const restored = context.stored.models.filter(
(model): model is Model<"openai-completions"> =>
model.provider === LLAMA_PROVIDER_ID && model.api === "openai-completions",
);
if (
!(await context.publish({
update: () => {
models = restored;
},
}))
) {
return;
}
}
if (!context.allowNetwork || context.signal.aborted || context.credential?.type !== "api_key") return;
const serverUrl = credentialServerUrl(context.credential);
if (!serverUrl) return;
const client = new LlamaClient(serverUrl, context.credential.key);
const catalog = await client.list({ signal: context.signal });
if (context.signal.aborted) return;
const routerAutoload = await routerAutoloadEnabled(client, catalog, context.signal);
if (context.signal.aborted) return;
const refreshed = catalog
.filter((model) => modelIsSelectable(model, routerAutoload))
.map((model) => toPiModel(model, serverUrl));
await context.publish({
persist: { models: refreshed, checkedAt: Date.now() },
update: () => {
models = refreshed;
},
});
},
stream: (model, context, options) => stream(model, context, options as ProviderStreamOptions | undefined),
streamSimple: (model, context, options) => streamSimple(model, context, options),
};
return { provider, setCatalog };
}

View File

@@ -0,0 +1,542 @@
import {
type Component,
Container,
type Focusable,
fuzzyFilter,
Input,
type SelectItem,
SelectList,
Spacer,
Text,
type TUI,
truncateToWidth,
visibleWidth,
} from "@earendil-works/pi-tui";
import type { ExtensionCommandContext } from "../../core/extensions/types.ts";
import type { KeybindingsManager } from "../../core/keybindings.ts";
import { DynamicBorder } from "../../modes/interactive/components/dynamic-border.ts";
import { keyHint } from "../../modes/interactive/components/keybinding-hints.ts";
import type { Theme } from "../../modes/interactive/theme/theme.ts";
import type { LlamaModelInfo, LlamaProgress } from "./client.ts";
import type { HuggingFaceModel } from "./huggingface.ts";
const DOWNLOAD_VALUE = "\0download";
export type LlamaManagerAction = { type: "model"; model: LlamaModelInfo } | { type: "download" } | { type: "close" };
interface ProgressState extends LlamaProgress {
title: string;
model: string;
}
function contextLabel(model: LlamaModelInfo): string | undefined {
const context = model.meta?.n_ctx ?? model.meta?.n_ctx_train;
if (context) return context >= 1000 ? `${Math.round(context / 1000)}k` : String(context);
const args = model.status.args ?? [];
for (let index = 0; index < args.length - 1; index++) {
if (args[index] !== "--ctx-size" && args[index] !== "-c" && args[index] !== "-ctx") continue;
const value = Number(args[index + 1]);
if (Number.isFinite(value) && value > 0) return value >= 1000 ? `${Math.round(value / 1000)}k` : String(value);
}
return undefined;
}
function modelDescription(model: LlamaModelInfo): string {
const details: string[] = [];
const loaded = model.status.value === "loaded" || model.status.value === "sleeping";
if (loaded) details.push("loaded");
else if (model.status.value !== "unloaded") details.push(model.status.value);
const context = loaded ? contextLabel(model) : undefined;
if (context) details.push(`${context} context`);
return details.join(" · ");
}
function selectTheme(theme: Theme) {
return {
selectedPrefix: (text: string) => theme.fg("accent", text),
selectedText: (text: string) => theme.fg("accent", text),
description: (text: string) => theme.fg("muted", text),
scrollInfo: (text: string) => theme.fg("dim", text),
noMatch: (text: string) => theme.fg("warning", text),
};
}
function frame(theme: Theme, title: string, body: Component[], footer?: string): Container {
const container = new Container();
container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
container.addChild(new Text(theme.fg("accent", theme.bold(title)), 1, 0));
for (const child of body) container.addChild(child);
if (footer) {
container.addChild(new Spacer(1));
container.addChild(new Text(theme.fg("dim", footer), 1, 0));
}
container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
return container;
}
export interface LlamaUi {
showModels(serverUrl: string, models: LlamaModelInfo[]): Promise<LlamaManagerAction>;
select(title: string, options: string[]): Promise<string | undefined>;
confirm(title: string, message: string): Promise<boolean>;
connectionError(serverUrl: string, message: string): Promise<"retry" | "close">;
searchModels(
search: (query: string, signal: AbortSignal) => Promise<HuggingFaceModel[]>,
): Promise<string | undefined>;
showStatus(title: string, message: string): void;
progress(state: ProgressState): Promise<void>;
updateProgress(state: ProgressState): void;
}
function compactCount(value: number): string {
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(value >= 10_000_000 ? 0 : 1)}M`;
if (value >= 1_000) return `${(value / 1_000).toFixed(value >= 100_000 ? 0 : 1)}k`;
return String(value);
}
class HuggingFaceSearch extends Container implements Focusable {
private readonly tui: TUI;
private readonly theme: Theme;
private readonly keybindings: KeybindingsManager;
private readonly search: (query: string, signal: AbortSignal) => Promise<HuggingFaceModel[]>;
private readonly cache: Map<string, HuggingFaceModel[]>;
private readonly onSelectModel: (model: string | undefined) => void;
private readonly input = new Input();
private readonly resultsContainer = new Container();
private results: HuggingFaceModel[] = [];
private filteredResults: HuggingFaceModel[] = [];
private selectedIndex = 0;
private query = "";
private status = "Type at least 2 characters";
private debounce: ReturnType<typeof setTimeout> | undefined;
private request: AbortController | undefined;
private closed = false;
private _focused = false;
constructor(
tui: TUI,
theme: Theme,
keybindings: KeybindingsManager,
search: (query: string, signal: AbortSignal) => Promise<HuggingFaceModel[]>,
cache: Map<string, HuggingFaceModel[]>,
onSelectModel: (model: string | undefined) => void,
) {
super();
this.tui = tui;
this.theme = theme;
this.keybindings = keybindings;
this.search = search;
this.cache = cache;
this.onSelectModel = onSelectModel;
this.addChild(new Text(theme.fg("dim", "Model name or owner/repository[:quant]"), 1, 0));
this.addChild(this.input);
this.addChild(new Spacer(1));
this.addChild(this.resultsContainer);
this.updateResults();
}
get focused(): boolean {
return this._focused;
}
set focused(value: boolean) {
this._focused = value;
this.input.focused = value;
}
private updateResults(): void {
this.resultsContainer.clear();
const maxVisible = 10;
const start = Math.max(
0,
Math.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredResults.length - maxVisible),
);
const end = Math.min(start + maxVisible, this.filteredResults.length);
for (let index = start; index < end; index++) {
const model = this.filteredResults[index];
if (!model) continue;
const prefix = index === this.selectedIndex ? "→ " : " ";
const details = `${compactCount(model.downloads)} downloads`;
this.resultsContainer.addChild(
new Text(
index === this.selectedIndex
? this.theme.fg("accent", `${prefix}${model.id} ${details}`)
: `${prefix}${model.id}${this.theme.fg("muted", ` ${details}`)}`,
0,
0,
),
);
}
if (start > 0 || end < this.filteredResults.length) {
this.resultsContainer.addChild(
new Text(this.theme.fg("dim", ` (${this.selectedIndex + 1}/${this.filteredResults.length})`), 0, 0),
);
}
if (this.filteredResults.length === 0) {
this.resultsContainer.addChild(new Text(this.theme.fg("dim", ` ${this.status}`), 0, 0));
} else if (this.status === "Searching Hugging Face…") {
this.resultsContainer.addChild(new Text(this.theme.fg("dim", ` ${this.status}`), 0, 0));
}
this.tui.requestRender();
}
private filterResults(): void {
if (this.query) {
const matches = new Set(fuzzyFilter(this.results, this.query, (model) => model.id).map((model) => model.id));
this.filteredResults = this.results.filter((model) => matches.has(model.id));
} else {
this.filteredResults = this.results;
}
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredResults.length - 1));
this.updateResults();
}
private scheduleSearch(): void {
if (this.debounce) clearTimeout(this.debounce);
this.request?.abort();
this.request = undefined;
if (this.query.length < 2) {
this.status = "Type at least 2 characters";
this.filterResults();
return;
}
const cached = this.cache.get(this.query.toLowerCase());
if (cached) {
this.results = cached;
this.status = cached.length === 0 ? "No GGUF models found" : "";
this.filterResults();
return;
}
this.status = "Searching Hugging Face…";
this.filterResults();
this.debounce = setTimeout(() => void this.runSearch(this.query), 500);
}
private async runSearch(query: string): Promise<void> {
const request = new AbortController();
this.request = request;
try {
const results = await this.search(query, request.signal);
this.cache.set(query.toLowerCase(), results);
if (this.closed || request.signal.aborted || this.query !== query) return;
this.results = results;
this.selectedIndex = 0;
this.status = results.length === 0 ? "No GGUF models found" : "";
this.filterResults();
} catch (error) {
if (this.closed || request.signal.aborted || this.query !== query) return;
this.results = [];
this.status = error instanceof Error ? error.message : String(error);
this.filterResults();
} finally {
if (this.request === request) this.request = undefined;
}
}
private close(model: string | undefined): void {
if (this.closed) return;
this.closed = true;
if (this.debounce) clearTimeout(this.debounce);
this.request?.abort();
this.onSelectModel(model);
}
handleInput(data: string): void {
if (this.keybindings.matches(data, "tui.select.up")) {
if (this.filteredResults.length > 0) {
this.selectedIndex = this.selectedIndex === 0 ? this.filteredResults.length - 1 : this.selectedIndex - 1;
this.updateResults();
}
return;
}
if (this.keybindings.matches(data, "tui.select.down")) {
if (this.filteredResults.length > 0) {
this.selectedIndex = this.selectedIndex === this.filteredResults.length - 1 ? 0 : this.selectedIndex + 1;
this.updateResults();
}
return;
}
if (this.keybindings.matches(data, "tui.select.confirm")) {
const exact = /^[^/\s]+\/[^:\s]+(?::[^\s:]+)?$/u.test(this.query) ? this.query : undefined;
const selected = exact ?? this.filteredResults[this.selectedIndex]?.id;
if (selected) this.close(selected);
return;
}
if (this.keybindings.matches(data, "tui.select.cancel")) {
this.close(undefined);
return;
}
this.input.handleInput(data);
const query = this.input.getValue().trim();
if (query === this.query) return;
this.query = query;
this.scheduleSearch();
}
}
class LlamaView implements LlamaUi, Focusable {
private readonly tui: TUI;
private readonly theme: Theme;
private readonly keybindings: KeybindingsManager;
private readonly searchCache = new Map<string, HuggingFaceModel[]>();
private content: Container;
private inputHandler: { handleInput?(data: string): void } | undefined;
private inputTarget: Focusable | undefined;
private progressPromise: Promise<void> | undefined;
private progressResolver: (() => void) | undefined;
private showingProgress = false;
private _focused = false;
constructor(tui: TUI, theme: Theme, keybindings: KeybindingsManager) {
this.tui = tui;
this.theme = theme;
this.keybindings = keybindings;
this.content = frame(theme, "llama.cpp models", [new Text(theme.fg("muted", "Loading…"), 1, 1)]);
}
get focused(): boolean {
return this._focused;
}
set focused(value: boolean) {
this._focused = value;
if (this.inputTarget) this.inputTarget.focused = value;
}
private setContent(
content: Container,
inputHandler?: { handleInput?(data: string): void },
inputTarget?: Focusable,
): void {
if (this.inputTarget) this.inputTarget.focused = false;
this.progressPromise = undefined;
this.progressResolver = undefined;
this.showingProgress = false;
this.content = content;
this.inputHandler = inputHandler;
this.inputTarget = inputTarget;
if (this.inputTarget) this.inputTarget.focused = this._focused;
this.tui.requestRender();
}
showModels(serverUrl: string, models: LlamaModelInfo[]): Promise<LlamaManagerAction> {
const sorted = [...models].sort((left, right) => {
const loaded = Number(right.status.value === "loaded") - Number(left.status.value === "loaded");
return loaded || left.id.localeCompare(right.id);
});
const byId = new Map(sorted.map((model) => [model.id, model]));
const items: SelectItem[] = [
...sorted.map((model) => ({
value: model.id,
label: model.id,
description: modelDescription(model),
})),
{ value: DOWNLOAD_VALUE, label: "Download model…", description: "Hugging Face owner/repository[:quant]" },
];
return new Promise((resolve) => {
const list = new SelectList(items, Math.min(items.length, 12), selectTheme(this.theme), {
minPrimaryColumnWidth: 36,
maxPrimaryColumnWidth: 56,
});
list.onSelect = (item) => {
if (item.value === DOWNLOAD_VALUE) resolve({ type: "download" });
else {
const model = byId.get(item.value);
if (model) resolve({ type: "model", model });
}
};
list.onCancel = () => resolve({ type: "close" });
this.setContent(
frame(
this.theme,
"llama.cpp models",
[new Text(this.theme.fg("dim", serverUrl), 1, 0), new Spacer(1), list],
`${keyHint("tui.select.confirm", "load/unload/download")} • ${keyHint("tui.select.cancel", "close")}`,
),
list,
);
});
}
select(title: string, options: string[]): Promise<string | undefined> {
return new Promise((resolve) => {
const list = new SelectList(
options.map((option) => ({ value: option, label: option })),
Math.min(options.length, 12),
selectTheme(this.theme),
);
list.onSelect = (item) => resolve(item.value);
list.onCancel = () => resolve(undefined);
this.setContent(
frame(
this.theme,
title,
[new Spacer(1), list],
`${keyHint("tui.select.confirm", "select")} • ${keyHint("tui.select.cancel", "cancel")}`,
),
list,
);
});
}
async confirm(title: string, message: string): Promise<boolean> {
return (await this.select(`${title}\n${message}`, ["Yes", "No"])) === "Yes";
}
async connectionError(serverUrl: string, message: string): Promise<"retry" | "close"> {
const choice = await this.select(`llama.cpp unavailable\n${serverUrl}\n\n${message}`, ["Retry", "Close"]);
return choice === "Retry" ? "retry" : "close";
}
searchModels(
search: (query: string, signal: AbortSignal) => Promise<HuggingFaceModel[]>,
): Promise<string | undefined> {
return new Promise((resolve) => {
const component = new HuggingFaceSearch(
this.tui,
this.theme,
this.keybindings,
search,
this.searchCache,
resolve,
);
this.setContent(
frame(
this.theme,
"Download model",
[new Spacer(1), component],
`${keyHint("tui.select.confirm", "select")} • ${keyHint("tui.select.cancel", "back")}`,
),
component,
component,
);
});
}
showStatus(title: string, message: string): void {
this.setContent(frame(this.theme, title, [new Spacer(1), new Text(this.theme.fg("muted", message), 1, 0)]));
}
progress(state: ProgressState): Promise<void> {
if (!this.progressPromise) {
this.progressPromise = new Promise((resolve) => {
this.progressResolver = resolve;
});
}
this.showingProgress = true;
this.updateProgress(state);
return this.progressPromise;
}
updateProgress(state: ProgressState): void {
if (!this.showingProgress) return;
const body = [
new Text(this.theme.fg("text", state.model), 1, 0),
new Spacer(1),
new Text(this.theme.fg("muted", state.message), 1, 0),
];
if (state.ratio !== undefined) {
const available = 40;
const filled = Math.round(Math.max(0, Math.min(1, state.ratio)) * available);
body.push(
new Text(
this.theme.fg(
"accent",
`${"█".repeat(filled)}${"─".repeat(available - filled)} ${Math.round(state.ratio * 100)}%`,
),
1,
0,
),
);
}
if (state.detail) body.push(new Text(this.theme.fg("dim", state.detail), 1, 0));
this.content = frame(this.theme, state.title, body, keyHint("tui.select.cancel", "stop"));
this.inputHandler = undefined;
this.tui.requestRender();
}
handleInput(data: string): void {
if (this.progressResolver && this.keybindings.matches(data, "tui.select.cancel")) {
const resolve = this.progressResolver;
this.progressPromise = undefined;
this.progressResolver = undefined;
resolve();
return;
}
this.inputHandler?.handleInput?.(data);
this.tui.requestRender();
}
render(width: number): string[] {
return this.content
.render(width)
.map((line) => (visibleWidth(line) > width ? truncateToWidth(line, width, "") : line));
}
invalidate(): void {
this.content.invalidate();
}
}
export async function showLlamaUi(ctx: ExtensionCommandContext, run: (ui: LlamaUi) => Promise<void>): Promise<void> {
await ctx.ui.custom<void>((tui, theme, keybindings, done) => {
const view = new LlamaView(tui, theme, keybindings);
void run(view).then(
() => done(),
(error: unknown) => {
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
done();
},
);
return view;
});
}
export async function runWithProgress<T>(
ui: LlamaUi,
options: {
title: string;
model: string;
initialMessage: string;
cancelTitle: string;
cancelMessage: string;
run(signal: AbortSignal, update: (progress: LlamaProgress) => void): Promise<T>;
cancel(): Promise<void>;
},
): Promise<{ cancelled: true } | { cancelled: false; value: T }> {
const controller = new AbortController();
const state: ProgressState = { title: options.title, model: options.model, message: options.initialMessage };
const settled = options
.run(controller.signal, (progress) => {
Object.assign(state, progress);
ui.updateProgress(state);
})
.then(
(value) => ({ ok: true as const, value }),
(error: unknown) => ({ ok: false as const, error }),
);
let completed = false;
settled.finally(() => {
completed = true;
});
while (!completed) {
const outcome = await Promise.race([
settled.then(() => "settled" as const),
ui.progress(state).then(() => "stop" as const),
]);
if (outcome === "settled") break;
const stop = await ui.confirm(options.cancelTitle, options.cancelMessage);
if (!stop || completed) continue;
try {
await options.cancel();
} finally {
controller.abort(new Error("Cancelled"));
}
await settled;
return { cancelled: true };
}
const result = await settled;
if (!result.ok) throw result.error;
return { cancelled: false, value: result.value };
}