Files
oh-my-pi/packages/coding-agent/src/cli/tiny-models-cli.ts
T
can1357 826c3b932d refactor(packages/coding-agent): reorganized tiny-title runtime stack
- Added tiny-title protocol contracts, including progress-state unions, message payloads, and transport interfaces.
- Added title text utilities to truncate long inputs, wrap `<user-message>` blocks, and normalize generated titles.
- Added tiny-title model registry and helpers with type-safe keys and runtime optional loading via optionalDependencies.
- Added client-side worker orchestration with spawn fallback, request queueing, progress/error routing, and smoke-test APIs.
- Added worker runtime for model resolution, prompt-based inference, lock-based install retries, and close-time cache clear.
2026-05-30 17:40:18 +02:00

129 lines
4.4 KiB
TypeScript

import { formatBytes } from "@oh-my-pi/pi-utils";
import chalk from "chalk";
import {
DEFAULT_TINY_TITLE_MODEL_KEY,
getTinyTitleModelSpec,
isTinyTitleLocalModelKey,
TINY_TITLE_LOCAL_MODELS,
type TinyTitleLocalModelKey,
} from "../tiny/models";
import { shutdownTinyTitleClient, tinyTitleClient } from "../tiny/title-client";
import type { TinyTitleProgressEvent } from "../tiny/title-protocol";
export type TinyModelsAction = "download" | "list";
export interface TinyModelsCommandArgs {
action: TinyModelsAction;
model?: string;
flags: {
json?: boolean;
};
}
interface ProgressReporter {
onProgress(event: TinyTitleProgressEvent): void;
finish(ok: boolean): void;
}
interface DownloadResult {
model: TinyTitleLocalModelKey;
ok: boolean;
}
function writeLine(text = ""): void {
process.stdout.write(`${text}\n`);
}
function resolveModels(model: string | undefined): TinyTitleLocalModelKey[] {
if (!model) return [DEFAULT_TINY_TITLE_MODEL_KEY];
if (model === "all") return TINY_TITLE_LOCAL_MODELS.map(spec => spec.key);
if (!isTinyTitleLocalModelKey(model)) {
const values = TINY_TITLE_LOCAL_MODELS.map(spec => spec.key).join(", ");
throw new Error(`Unknown tiny title model: ${model}. Expected one of: ${values}, all`);
}
return [model];
}
function listModels(json: boolean | undefined): void {
if (json) {
writeLine(JSON.stringify({ models: TINY_TITLE_LOCAL_MODELS }));
return;
}
writeLine(chalk.bold("Tiny title models"));
for (const spec of TINY_TITLE_LOCAL_MODELS) {
const defaultMark = spec.key === DEFAULT_TINY_TITLE_MODEL_KEY ? chalk.cyan(" default") : "";
writeLine(`${chalk.cyan(spec.key)}${defaultMark}`);
writeLine(` ${spec.label} — ${spec.description}`);
}
}
function makeProgressReporter(modelKey: TinyTitleLocalModelKey, json: boolean | undefined): ProgressReporter {
if (json || !process.stdout.isTTY) {
return { onProgress: () => undefined, finish: () => undefined };
}
const spec = getTinyTitleModelSpec(modelKey);
let lastWidth = 0;
let lastProgress = -1;
const render = (event: TinyTitleProgressEvent): void => {
const progress = event.progress ?? lastProgress;
if (progress >= 0 && progress < lastProgress + 1 && event.status !== "ready") return;
if (progress >= 0) lastProgress = progress;
const ratio = progress >= 0 ? Math.max(0, Math.min(1, progress / 100)) : 0;
const barWidth = 30;
const filled = Math.round(ratio * barWidth);
const bar = `${"█".repeat(filled)}${"░".repeat(barWidth - filled)}`;
const pct = progress >= 0 ? `${Math.floor(progress).toString().padStart(3, " ")}%` : " --%";
const bytes = event.loaded && event.total ? ` ${formatBytes(event.loaded)}/${formatBytes(event.total)}` : "";
const file = event.file ? ` ${event.file.split("/").at(-1) ?? event.file}` : "";
const label = event.status === "ready" ? "Ready" : "Downloading";
const line = `${chalk.cyan(label)} ${spec.label} [${bar}] ${pct}${bytes}${file}`;
process.stdout.write(`\r${line.padEnd(lastWidth)}`);
lastWidth = line.length;
};
return {
onProgress(event) {
if (event.modelKey !== modelKey) return;
render(event);
},
finish(ok) {
const suffix = ok ? chalk.green("done") : chalk.red("failed");
process.stdout.write(`\r${`${spec.label}: ${suffix}`.padEnd(lastWidth)}\n`);
},
};
}
async function downloadOne(modelKey: TinyTitleLocalModelKey, json: boolean | undefined): Promise<DownloadResult> {
const spec = getTinyTitleModelSpec(modelKey);
if (!json && !process.stdout.isTTY) writeLine(`Downloading ${spec.label} (${modelKey})...`);
const progress = makeProgressReporter(modelKey, json);
const ok = await tinyTitleClient.downloadModel(modelKey, { onProgress: progress.onProgress });
progress.finish(ok);
if (!json && !process.stdout.isTTY)
writeLine(ok ? `Downloaded ${spec.label}.` : `Failed to download ${spec.label}.`);
return { model: modelKey, ok };
}
export async function runTinyModelsCommand(command: TinyModelsCommandArgs): Promise<void> {
if (command.action === "list") {
listModels(command.flags.json);
return;
}
const models = resolveModels(command.model);
const results: DownloadResult[] = [];
try {
for (const model of models) {
results.push(await downloadOne(model, command.flags.json));
}
} finally {
await shutdownTinyTitleClient();
}
if (command.flags.json) {
writeLine(JSON.stringify({ results }));
}
if (results.some(result => !result.ok)) {
throw new Error("One or more tiny title models failed to download");
}
}