102 lines
3.0 KiB
TypeScript
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 [];
|
|
}
|
|
}
|