Since Node >= 24 is the minimum engine, remove the better-sqlite3 fallback
chain from sf-db.ts, unit-ownership.ts, and cli-stats.ts. Use DatabaseSync
from node:sqlite directly. Also replace the `glob` npm package with built-in
node:fs/promises.glob and node:fs.globSync in pi-coding-agent LSP utils.
- Remove createRequire boilerplate and suppressSqliteWarning helper
- Simplify loadProvider() and openRawDb()
- Net -177 lines of fallback/middleware code
💘 Generated with Crush
Assisted-by: GLM-5.1 via Crush <crush@charm.land>
259 lines
6.4 KiB
TypeScript
259 lines
6.4 KiB
TypeScript
import { existsSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
|
|
export interface ModelStatsRow {
|
|
model_id: string;
|
|
provider: string;
|
|
unit_type: string;
|
|
attempts: number;
|
|
successes: number;
|
|
avg_ms: number | null;
|
|
avg_cost: number | null;
|
|
avg_retries: number | null;
|
|
}
|
|
|
|
export interface QueryModelStatsOptions {
|
|
sinceSeconds: number;
|
|
unitType?: string;
|
|
}
|
|
|
|
interface SqliteStatement {
|
|
all(...params: unknown[]): Record<string, unknown>[];
|
|
}
|
|
|
|
interface SqliteDb {
|
|
prepare(sql: string): SqliteStatement;
|
|
close?: () => void;
|
|
}
|
|
|
|
interface ParsedStatsArgs {
|
|
command: "models";
|
|
sinceSeconds: number;
|
|
unitType?: string;
|
|
}
|
|
|
|
|
|
export function parseDurationSeconds(value: string): number {
|
|
const match = value.trim().match(/^(\d+(?:\.\d+)?)([smhdw])$/i);
|
|
if (!match) {
|
|
throw new Error(`Invalid duration: ${value}`);
|
|
}
|
|
const amount = Number(match[1]);
|
|
const unit = match[2].toLowerCase();
|
|
const multiplier =
|
|
unit === "s"
|
|
? 1
|
|
: unit === "m"
|
|
? 60
|
|
: unit === "h"
|
|
? 60 * 60
|
|
: unit === "d"
|
|
? 24 * 60 * 60
|
|
: 7 * 24 * 60 * 60;
|
|
return Math.max(1, Math.floor(amount * multiplier));
|
|
}
|
|
|
|
function toNumber(value: unknown, fallback = 0): number {
|
|
const n = Number(value);
|
|
return Number.isFinite(n) ? n : fallback;
|
|
}
|
|
|
|
function toNullableNumber(value: unknown): number | null {
|
|
if (value === null || value === undefined) return null;
|
|
const n = Number(value);
|
|
return Number.isFinite(n) ? n : null;
|
|
}
|
|
|
|
export function queryModelStats(
|
|
db: SqliteDb,
|
|
options: QueryModelStatsOptions,
|
|
): ModelStatsRow[] {
|
|
const where = ["recorded_at > unixepoch() - ?"];
|
|
const params: unknown[] = [options.sinceSeconds];
|
|
if (options.unitType) {
|
|
where.push("unit_type = ?");
|
|
params.push(options.unitType);
|
|
}
|
|
|
|
const sql = `
|
|
SELECT model_id, provider, unit_type, COUNT(*) AS attempts,
|
|
SUM(succeeded) AS successes,
|
|
AVG(duration_ms) AS avg_ms,
|
|
AVG(cost_usd) AS avg_cost,
|
|
AVG(retries) AS avg_retries
|
|
FROM llm_task_outcomes
|
|
WHERE ${where.join(" AND ")}
|
|
GROUP BY model_id, unit_type
|
|
ORDER BY attempts DESC
|
|
`;
|
|
|
|
return db
|
|
.prepare(sql)
|
|
.all(...params)
|
|
.map((row) => ({
|
|
model_id: String(row.model_id ?? ""),
|
|
provider: String(row.provider ?? ""),
|
|
unit_type: String(row.unit_type ?? ""),
|
|
attempts: toNumber(row.attempts),
|
|
successes: toNumber(row.successes),
|
|
avg_ms: toNullableNumber(row.avg_ms),
|
|
avg_cost: toNullableNumber(row.avg_cost),
|
|
avg_retries: toNullableNumber(row.avg_retries),
|
|
}));
|
|
}
|
|
|
|
function formatSuccessRate(row: ModelStatsRow): string {
|
|
if (row.attempts <= 0) return "0.0%";
|
|
return `${((row.successes / row.attempts) * 100).toFixed(1)}%`;
|
|
}
|
|
|
|
function formatMs(value: number | null): string {
|
|
return value === null ? "n/a" : value.toFixed(0);
|
|
}
|
|
|
|
function formatCost(value: number | null): string {
|
|
return value === null ? "n/a" : `$${value.toFixed(4)}`;
|
|
}
|
|
|
|
function formatRetries(value: number | null): string {
|
|
return value === null ? "n/a" : value.toFixed(2);
|
|
}
|
|
|
|
function pad(value: string, width: number, align: "left" | "right"): string {
|
|
return align === "right" ? value.padStart(width) : value.padEnd(width);
|
|
}
|
|
|
|
export function formatModelStatsTable(rows: ModelStatsRow[]): string {
|
|
const headers = [
|
|
"model_id",
|
|
"provider",
|
|
"unit_type",
|
|
"attempts",
|
|
"success_rate",
|
|
"avg_ms",
|
|
"avg_cost",
|
|
"avg_retries",
|
|
];
|
|
const body = rows.map((row) => [
|
|
row.model_id,
|
|
row.provider,
|
|
row.unit_type,
|
|
String(row.attempts),
|
|
formatSuccessRate(row),
|
|
formatMs(row.avg_ms),
|
|
formatCost(row.avg_cost),
|
|
formatRetries(row.avg_retries),
|
|
]);
|
|
const widths = headers.map((header, index) =>
|
|
Math.max(header.length, ...body.map((row) => row[index].length)),
|
|
);
|
|
const numeric = new Set([3, 4, 5, 6, 7]);
|
|
|
|
const separator = `+${widths.map((width) => "-".repeat(width + 2)).join("+")}+`;
|
|
const renderRow = (row: string[]) =>
|
|
`| ${row.map((cell, index) => pad(cell, widths[index], numeric.has(index) ? "right" : "left")).join(" | ")} |`;
|
|
|
|
return (
|
|
[
|
|
separator,
|
|
renderRow(headers),
|
|
separator,
|
|
...body.map(renderRow),
|
|
separator,
|
|
].join("\n") + "\n"
|
|
);
|
|
}
|
|
|
|
function parseStatsArgs(argv: string[]): ParsedStatsArgs | null {
|
|
const args = argv.slice(1);
|
|
if (args[0] !== "models") return null;
|
|
const parsed: ParsedStatsArgs = {
|
|
command: "models",
|
|
sinceSeconds: parseDurationSeconds("7d"),
|
|
};
|
|
|
|
for (let i = 1; i < args.length; i++) {
|
|
const arg = args[i];
|
|
if (arg === "--unit-type" && i + 1 < args.length) {
|
|
parsed.unitType = args[++i];
|
|
} else if (arg.startsWith("--unit-type=")) {
|
|
parsed.unitType = arg.slice("--unit-type=".length);
|
|
} else if (arg === "--since" && i + 1 < args.length) {
|
|
parsed.sinceSeconds = parseDurationSeconds(args[++i]);
|
|
} else if (arg.startsWith("--since=")) {
|
|
parsed.sinceSeconds = parseDurationSeconds(arg.slice("--since=".length));
|
|
}
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
function usage(): string {
|
|
return (
|
|
[
|
|
"Usage: sf stats models [--unit-type <type>] [--since <duration>]",
|
|
"",
|
|
"Examples:",
|
|
" sf stats models --since 7d",
|
|
" sf stats models --unit-type execute-task --since 24h",
|
|
].join("\n") + "\n"
|
|
);
|
|
}
|
|
|
|
function openSqliteDb(dbPath: string): SqliteDb {
|
|
return new DatabaseSync(dbPath, { readOnly: true }) as SqliteDb;
|
|
}
|
|
|
|
export async function runStatsCli(
|
|
argv: string[],
|
|
deps: {
|
|
basePath: string;
|
|
stdout?: Pick<typeof process.stdout, "write">;
|
|
stderr?: Pick<typeof process.stderr, "write">;
|
|
},
|
|
): Promise<number> {
|
|
const stdout = deps.stdout ?? process.stdout;
|
|
const stderr = deps.stderr ?? process.stderr;
|
|
let parsed: ParsedStatsArgs | null;
|
|
try {
|
|
parsed = parseStatsArgs(argv);
|
|
} catch (err) {
|
|
stderr.write(
|
|
`sf stats: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
);
|
|
return 1;
|
|
}
|
|
if (!parsed) {
|
|
stderr.write(usage());
|
|
return 1;
|
|
}
|
|
|
|
const dbPath = join(deps.basePath, ".sf", "sf.db");
|
|
if (!existsSync(dbPath)) {
|
|
stderr.write(`sf stats: database not found at ${dbPath}\n`);
|
|
return 1;
|
|
}
|
|
|
|
let db: SqliteDb | null = null;
|
|
try {
|
|
db = openSqliteDb(dbPath);
|
|
const rows = queryModelStats(db, parsed);
|
|
if (rows.length === 0) {
|
|
stdout.write("No model outcomes recorded for the selected window.\n");
|
|
} else {
|
|
stdout.write(formatModelStatsTable(rows));
|
|
}
|
|
return 0;
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
if (/no such table/i.test(message)) {
|
|
stdout.write("No model outcomes recorded yet.\n");
|
|
return 0;
|
|
}
|
|
stderr.write(`sf stats: ${message}\n`);
|
|
return 1;
|
|
} finally {
|
|
db?.close?.();
|
|
}
|
|
}
|