import { promptForModel } from './prompts.js'; import type { AccessKind, ModelCandidate, ModelRegistryView, OkraPiConfig, PiModel, PromptProfile, } from './types.js'; export function fullModelId(model: Pick): string { return `${model.provider}/${model.id}`; } function isLocalModel(model: PiModel): boolean { const provider = model.provider.toLowerCase(); if (provider.includes('ollama') || provider.includes('lmstudio') || provider.includes('local')) { return true; } try { const hostname = new URL(model.baseUrl).hostname.toLowerCase(); return ( hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '0.0.0.0' || hostname === '::1' || hostname.endsWith('.local') ); } catch { return false; } } function hasKnownPrice(model: PiModel): boolean { return ( model.cost.input > 0 || model.cost.output > 0 || model.cost.cacheRead > 0 || model.cost.cacheWrite > 0 ); } export function accessForModel(model: PiModel, registry: ModelRegistryView): AccessKind { if (registry.isUsingOAuth(model)) return 'subscription'; if (isLocalModel(model)) return 'local'; if (hasKnownPrice(model)) return 'metered'; return 'unknown'; } export function estimateModelPageCost( model: PiModel, prompt: PromptProfile, ): number | undefined { if (!hasKnownPrice(model)) return undefined; return ( (prompt.estimatedInputTokensPerPage * model.cost.input + prompt.estimatedOutputTokensPerPage * model.cost.output) / 1_000_000 ); } export function discoverModels(registry: ModelRegistryView): ModelCandidate[] { return registry .getAvailable() .filter((model) => model.input.includes('image')) .map((model) => { const prompt = promptForModel(model); const access = accessForModel(model, registry); const estimatedUsdPerPage = prompt && access === 'metered' ? estimateModelPageCost(model, prompt) : undefined; return { model, fullId: fullModelId(model), access, prompt, estimatedUsdPerPage, }; }) .sort((a, b) => a.fullId.localeCompare(b.fullId)); } function resolveReference( reference: string, candidates: readonly ModelCandidate[], aliases: Readonly>, ): ModelCandidate | undefined { const expanded = aliases[reference] ?? reference; const full = candidates.find((candidate) => candidate.fullId === expanded); if (full) return full; const byNativeId = candidates.filter((candidate) => candidate.model.id === expanded); return byNativeId.length === 1 ? byNativeId[0] : undefined; } function selectionRank(candidate: ModelCandidate): number { if (candidate.access === 'subscription') return 0; if (candidate.access === 'local') return 1; if (candidate.access === 'metered') return 2; return 3; } export function selectModel( registry: ModelRegistryView, options: { requested?: string; config?: OkraPiConfig } = {}, ): ModelCandidate { const candidates = discoverModels(registry); const aliases = options.config?.aliases ?? {}; const reference = options.requested ?? options.config?.defaultModel; if (reference) { const selected = resolveReference(reference, candidates, aliases); if (!selected) { throw new Error( `Model "${reference}" is not an authenticated Pi vision model. Run /pdf-parse models to see available models.`, ); } if (!selected.prompt) { throw new Error( `Model "${selected.fullId}" is authenticated but has no validated PDF parser prompt yet. Run /pdf-parse models to choose a ready pair.`, ); } return selected; } const ready = candidates.filter( (candidate): candidate is ModelCandidate & { prompt: PromptProfile } => candidate.prompt !== undefined, ); ready.sort((a, b) => { const access = selectionRank(a) - selectionRank(b); if (access !== 0) return access; const aCost = a.estimatedUsdPerPage ?? Number.POSITIVE_INFINITY; const bCost = b.estimatedUsdPerPage ?? Number.POSITIVE_INFINITY; return aCost - bCost || a.fullId.localeCompare(b.fullId); }); const selected = ready[0]; if (!selected) { const visionCount = candidates.length; throw new Error( visionCount === 0 ? 'Pi has no authenticated vision models. Configure one with /login, then run /pdf-parse models.' : `Pi has ${visionCount} authenticated vision model${visionCount === 1 ? '' : 's'}, but none has a validated PDF parser prompt yet. Run /pdf-parse models for details.`, ); } return selected; } export function costLabel(candidate: ModelCandidate): string { if (!candidate.prompt) return '—'; if (candidate.access === 'subscription') return 'subscription quota'; if (candidate.access === 'local') return '$0 local'; if (candidate.estimatedUsdPerPage !== undefined) { return `~$${candidate.estimatedUsdPerPage.toFixed(4)}`; } return 'unknown'; }