Files
platform/src/servers/api/pi/list-models.ts
T
2026-02-26 19:00:39 +00:00

102 lines
3.0 KiB
TypeScript

import { PI_CONFIG_DIR } from '../../data-path';
import { logger } from './logger';
import type { ModelInfo } from './types';
const CACHE_TTL_MS = 60_000;
let cachedModels: ModelInfo[] | null = null;
let cacheTimestamp = 0;
export function invalidateModelCache(): void {
cachedModels = null;
cacheTimestamp = 0;
}
const parseSize = (s?: string): number => {
if (!s) return 128000;
const match = s.match(/^([\d.]+)([KMG])?$/i);
if (!match) return 128000;
const num = parseFloat(match[1]!);
const unit = (match[2] ?? '').toUpperCase();
if (unit === 'K') return Math.round(num * 1000);
if (unit === 'M') return Math.round(num * 1000000);
if (unit === 'G') return Math.round(num * 1000000000);
return Math.round(num);
};
export async function listPiModels(): Promise<ModelInfo[]> {
if (cachedModels && Date.now() - cacheTimestamp < CACHE_TTL_MS) {
return cachedModels;
}
try {
const proc = Bun.spawn(['pi', '--list-models'], {
stdout: 'pipe',
stderr: 'pipe',
env: { ...process.env, PI_CODING_AGENT_DIR: PI_CONFIG_DIR },
});
const output = await new Response(proc.stdout).text();
await proc.exited;
if (proc.exitCode !== 0) {
const stderr = await new Response(proc.stderr).text();
logger.error('pi --list-models failed', { exitCode: proc.exitCode, stderr: stderr.trim() });
return [];
}
const lines = output.trim().split('\n');
if (lines.length < 2) return [];
// Parse fixed-width table: provider, model, context, max-out, thinking, images
const header = lines[0]!;
const colStarts = [
header.indexOf('provider'),
header.indexOf('model'),
header.indexOf('context'),
header.indexOf('max-out'),
header.indexOf('thinking'),
header.indexOf('images'),
];
const extractCol = (line: string, colIdx: number): string => {
const start = colStarts[colIdx]!;
const end = colIdx < colStarts.length - 1 ? colStarts[colIdx + 1]! : line.length;
return line.slice(start, end).trim();
};
const models: ModelInfo[] = [];
for (let i = 1; i < lines.length; i++) {
const line = lines[i]!;
if (!line.trim()) continue;
const provider = extractCol(line, 0);
const model = extractCol(line, 1);
const context = extractCol(line, 2);
const maxOut = extractCol(line, 3);
const thinking = extractCol(line, 4);
const images = extractCol(line, 5);
const normalizedProvider = provider === 'zai' ? 'opencode' : provider;
models.push({
id: `${provider}/${model}`,
name: model,
provider: normalizedProvider,
contextWindow: parseSize(context),
maxTokens: parseSize(maxOut),
reasoning: thinking === 'yes',
images: images === 'yes',
});
}
logger.info('pi --list-models returned', { count: models.length });
cachedModels = models;
cacheTimestamp = Date.now();
return models;
} catch (err) {
logger.error('Failed to run pi --list-models', { error: String(err) });
return [];
}
}