UNPKG

aiwg

Version:

Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.

262 lines (224 loc) 6.63 kB
/** * Model Catalog Loader * * Loads and merges model catalogs from multiple sources: * - Builtin models (shipped with AIWG) * - Discovered models (from auto-refresh) * - Custom models (user-defined) * * Merge priority: custom > discovered > builtin * * @implements @.aiwg/architecture/enhanced-model-selection-design.md#9-model-catalog-system * @module src/catalog/loader */ import { readFile } from 'fs/promises'; import { existsSync } from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import type { ModelCatalog, CatalogModel, CatalogProvider, CatalogFilter, } from './types.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Get catalog directory in user's data directory */ function getCatalogDir(): string { const homeDir = process.env.HOME || process.env.USERPROFILE || ''; return path.join(homeDir, '.local/share/aiwg/catalog'); } /** * Load a catalog file, handling missing files gracefully */ async function loadCatalogFile(filePath: string): Promise<Partial<ModelCatalog> | null> { if (!existsSync(filePath)) { return null; } try { const content = await readFile(filePath, 'utf-8'); return JSON.parse(content); } catch (error) { console.warn(`Failed to load catalog from ${filePath}:`, error); return null; } } /** * Merge two model catalogs * Later catalog takes precedence for conflicts */ function mergeCatalogs( base: Partial<ModelCatalog>, overlay: Partial<ModelCatalog> | null ): Partial<ModelCatalog> { if (!overlay) { return base; } return { models: { ...(base.models || {}), ...(overlay.models || {}), }, providers: { ...(base.providers || {}), ...(overlay.providers || {}), }, metadata: overlay.metadata || base.metadata, }; } /** * Load the builtin models catalog */ async function loadBuiltinCatalog(): Promise<Partial<ModelCatalog>> { const builtinPath = path.join(__dirname, 'builtin-models.json'); const catalog = await loadCatalogFile(builtinPath); if (!catalog) { throw new Error('Failed to load builtin models catalog'); } return catalog; } /** * Load the discovered models catalog */ async function loadDiscoveredCatalog(): Promise<Partial<ModelCatalog> | null> { const catalogDir = getCatalogDir(); const discoveredPath = path.join(catalogDir, 'discovered-models.json'); return loadCatalogFile(discoveredPath); } /** * Load the custom models catalog */ async function loadCustomCatalog(): Promise<Partial<ModelCatalog> | null> { const catalogDir = getCatalogDir(); const customPath = path.join(catalogDir, 'custom-models.json'); return loadCatalogFile(customPath); } /** * Load and merge all catalog sources * * Merge order: builtin -> discovered -> custom * Later sources override earlier ones */ export async function loadCatalog(): Promise<ModelCatalog> { const builtin = await loadBuiltinCatalog(); const discovered = await loadDiscoveredCatalog(); const custom = await loadCustomCatalog(); let merged = mergeCatalogs(builtin, discovered); merged = mergeCatalogs(merged, custom); // Ensure required fields exist const catalog: ModelCatalog = { models: merged.models || {}, providers: merged.providers || {}, metadata: merged.metadata || { version: '1.0.0', lastRefresh: new Date().toISOString(), modelCount: 0, providerCount: 0, }, }; // Update counts catalog.metadata.modelCount = Object.keys(catalog.models).length; catalog.metadata.providerCount = Object.keys(catalog.providers).length; return catalog; } /** * Get a single model by ID or alias */ export async function getModel(modelIdOrAlias: string): Promise<CatalogModel | null> { const catalog = await loadCatalog(); // Try direct ID lookup if (catalog.models[modelIdOrAlias]) { return catalog.models[modelIdOrAlias]; } // Try alias lookup for (const model of Object.values(catalog.models)) { if (model.aliases.includes(modelIdOrAlias)) { return model; } } return null; } /** * List all models, optionally filtered */ export async function listModels(filter?: CatalogFilter): Promise<CatalogModel[]> { const catalog = await loadCatalog(); let models = Object.values(catalog.models); if (!filter) { return models; } // Apply filters if (filter.provider) { models = models.filter((m) => m.provider === filter.provider); } if (filter.status) { models = models.filter((m) => m.status === filter.status); } if (filter.tag) { models = models.filter((m) => m.tags.includes(filter.tag!)); } if (filter.minContext !== undefined) { const minCtx = filter.minContext; models = models.filter((m) => m.capabilities.contextWindow >= minCtx); } if (filter.capability) { const cap = filter.capability; models = models.filter((m) => { const value = m.capabilities[cap]; // For boolean capabilities, must be true if (typeof value === 'boolean') { return value === true; } // For quality ratings, exclude 'poor' and 'unknown' if (typeof value === 'string') { return value !== 'poor' && value !== 'unknown'; } // For numbers, must be > 0 return value > 0; }); } return models; } /** * Get provider information */ export async function getProvider(providerId: string): Promise<CatalogProvider | null> { const catalog = await loadCatalog(); return catalog.providers[providerId] || null; } /** * List all providers */ export async function listProviders(): Promise<Array<{ id: string; info: CatalogProvider }>> { const catalog = await loadCatalog(); return Object.entries(catalog.providers).map(([id, info]) => ({ id, info })); } /** * Search models by query string * Searches in: id, displayName, aliases, tags */ export async function searchModels(query: string): Promise<CatalogModel[]> { const catalog = await loadCatalog(); const lowerQuery = query.toLowerCase(); return Object.values(catalog.models).filter((model) => { // Search in ID if (model.id.toLowerCase().includes(lowerQuery)) { return true; } // Search in display name if (model.displayName.toLowerCase().includes(lowerQuery)) { return true; } // Search in aliases if (model.aliases.some((alias) => alias.toLowerCase().includes(lowerQuery))) { return true; } // Search in tags if (model.tags.some((tag) => tag.toLowerCase().includes(lowerQuery))) { return true; } return false; }); }