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 { 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 []; } }