| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { getDbInstance } from "./db/core"; |
| import { invalidateDbCache } from "./db/readCache"; |
| import { backupDbFile } from "./db/backup"; |
|
|
| |
|
|
| type PricingEntry = { |
| input: number; |
| output: number; |
| cached?: number; |
| cache_creation?: number; |
| reasoning?: number; |
| }; |
|
|
| type PricingModels = Record<string, PricingEntry>; |
| type PricingByProvider = Record<string, PricingModels>; |
|
|
| interface CapabilityEntry { |
| tool_call: boolean | null; |
| reasoning: boolean | null; |
| attachment: boolean | null; |
| structured_output: boolean | null; |
| temperature: boolean | null; |
| modalities_input: string; |
| modalities_output: string; |
| knowledge_cutoff: string | null; |
| release_date: string | null; |
| last_updated: string | null; |
| status: string | null; |
| family: string | null; |
| open_weights: boolean | null; |
| limit_context: number | null; |
| limit_input: number | null; |
| limit_output: number | null; |
| interleaved_field: string | null; |
| } |
|
|
| type CapabilitiesByProvider = Record<string, Record<string, CapabilityEntry>>; |
|
|
| interface SyncStatus { |
| enabled: boolean; |
| lastSync: string | null; |
| lastSyncModelCount: number; |
| lastSyncCapabilityCount: number; |
| nextSync: string | null; |
| intervalMs: number; |
| } |
|
|
| interface SyncResult { |
| success: boolean; |
| modelCount: number; |
| providerCount: number; |
| capabilityCount: number; |
| dryRun: boolean; |
| data?: { pricing: PricingByProvider; capabilities: CapabilitiesByProvider }; |
| error?: string; |
| } |
|
|
| |
|
|
| interface ModelsDevCost { |
| input?: number; |
| output?: number; |
| reasoning?: number; |
| cache_read?: number; |
| cache_write?: number; |
| input_audio?: number; |
| output_audio?: number; |
| } |
|
|
| interface ModelsDevLimit { |
| context?: number; |
| input?: number; |
| output?: number; |
| } |
|
|
| interface ModelsDevModalities { |
| input?: string[]; |
| output?: string[]; |
| } |
|
|
| interface ModelsDevInterleaved { |
| field?: string; |
| } |
|
|
| interface ModelsDevModel { |
| id: string; |
| name: string; |
| family?: string; |
| attachment?: boolean; |
| reasoning?: boolean; |
| tool_call?: boolean; |
| structured_output?: boolean; |
| temperature?: boolean; |
| knowledge?: string; |
| release_date?: string; |
| last_updated?: string; |
| open_weights?: boolean; |
| status?: string; |
| cost?: ModelsDevCost; |
| limit?: ModelsDevLimit; |
| modalities?: ModelsDevModalities; |
| interleaved?: ModelsDevInterleaved | boolean; |
| } |
|
|
| interface ModelsDevProvider { |
| id: string; |
| name?: string; |
| env?: string[]; |
| npm?: string; |
| api?: string; |
| doc?: string; |
| models: Record<string, ModelsDevModel>; |
| } |
|
|
| type ModelsDevData = Record<string, ModelsDevProvider>; |
|
|
| |
|
|
| const MODELS_DEV_API_URL = "https://models.dev/api.json"; |
|
|
| const parsedInterval = parseInt(process.env.MODELS_DEV_SYNC_INTERVAL || "86400", 10); |
| const SYNC_INTERVAL_MS = |
| Number.isFinite(parsedInterval) && parsedInterval > 0 ? parsedInterval * 1000 : 86400 * 1000; |
|
|
| |
| |
| |
| |
| |
| |
|
|
| const MODELS_DEV_PROVIDER_MAP: Record<string, string[]> = { |
| |
| openai: ["openai", "cx"], |
| anthropic: ["anthropic", "cc"], |
| google: ["gemini", "gemini-cli"], |
| vertex_ai: ["gemini", "vertex"], |
| deepseek: ["deepseek", "if"], |
| groq: ["groq"], |
| xai: ["xai"], |
| mistral: ["mistral"], |
| together_ai: ["together", "openrouter"], |
| fireworks: ["fireworks"], |
| cerebras: ["cerebras"], |
| cohere: ["cohere"], |
| nvidia: ["nvidia"], |
| nebius: ["nebius"], |
| siliconflow: ["siliconflow"], |
| hyperbolic: ["hyperbolic"], |
| huggingface: ["hf", "huggingface"], |
| openrouter: ["openrouter"], |
| perplexity: ["pplx", "perplexity"], |
| |
| bedrock: ["kiro", "kr"], |
| |
| alibaba: ["ali", "alibaba", "bcp", "alicode", "alicode-intl"], |
| zai: ["zai", "glm"], |
| moonshot: ["kimi", "kimi-coding", "kmc", "kmca"], |
| minimax: ["minimax", "minimax-cn"], |
| longcat: ["lc", "longcat"], |
| pollinations: ["pol", "pollinations"], |
| puter: ["pu", "puter"], |
| cloudflare: ["cf"], |
| scaleway: ["scw"], |
| ollama: ["ollamacloud", "ollama-cloud"], |
| blackbox: ["bb", "blackbox"], |
| kilocode: ["kc", "kilocode"], |
| cline: ["cl", "cline"], |
| cursor: ["cu", "cursor"], |
| github: ["gh", "github"], |
| |
| }; |
|
|
| |
| |
| |
| |
| export function mapProviderId(modelsDevProviderId: string): string[] { |
| return MODELS_DEV_PROVIDER_MAP[modelsDevProviderId] || [modelsDevProviderId]; |
| } |
|
|
| |
|
|
| let syncTimer: ReturnType<typeof setInterval> | null = null; |
| let lastSyncTime: string | null = null; |
| let lastSyncModelCount = 0; |
| let lastSyncCapabilityCount = 0; |
| let activeSyncIntervalMs = SYNC_INTERVAL_MS; |
| let cachedData: ModelsDevData | null = null; |
| let cacheTime = 0; |
| const CACHE_TTL_MS = 24 * 60 * 60 * 1000; |
|
|
| |
|
|
| |
| |
| |
| |
| export async function fetchModelsDev(): Promise<ModelsDevData> { |
| |
| if (cachedData && Date.now() - cacheTime < CACHE_TTL_MS) { |
| return cachedData; |
| } |
|
|
| const response = await fetch(MODELS_DEV_API_URL, { |
| signal: AbortSignal.timeout(30000), |
| }); |
| if (!response.ok) { |
| throw new Error(`models.dev fetch failed [${response.status}]: ${response.statusText}`); |
| } |
| const text = await response.text(); |
| try { |
| const data = JSON.parse(text) as ModelsDevData; |
| cachedData = data; |
| cacheTime = Date.now(); |
| return data; |
| } catch { |
| throw new Error(`models.dev returned invalid JSON (${text.slice(0, 100)}...)`); |
| } |
| } |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
| export function transformModelsDevToPricing(raw: ModelsDevData): PricingByProvider { |
| const result: PricingByProvider = {}; |
|
|
| for (const [providerId, providerData] of Object.entries(raw)) { |
| const omniRouteProviders = mapProviderId(providerId); |
|
|
| for (const [modelId, model] of Object.entries(providerData.models || {})) { |
| if (!model.cost) continue; |
|
|
| |
| if (model.cost.input == null) continue; |
|
|
| const entry: PricingEntry = { |
| input: model.cost.input, |
| output: model.cost.output ?? 0, |
| }; |
|
|
| if (model.cost.cache_read != null) { |
| entry.cached = model.cost.cache_read; |
| } |
| if (model.cost.cache_write != null) { |
| entry.cache_creation = model.cost.cache_write; |
| } |
| if (model.cost.reasoning != null) { |
| entry.reasoning = model.cost.reasoning; |
| } |
|
|
| |
| for (const omniProvider of omniRouteProviders) { |
| if (!result[omniProvider]) result[omniProvider] = {}; |
| result[omniProvider][modelId] = entry; |
| } |
| } |
| } |
|
|
| return result; |
| } |
|
|
| |
|
|
| |
| |
| |
| export function transformModelsDevToCapabilities(raw: ModelsDevData): CapabilitiesByProvider { |
| const result: CapabilitiesByProvider = {}; |
|
|
| for (const [providerId, providerData] of Object.entries(raw)) { |
| const omniRouteProviders = mapProviderId(providerId); |
|
|
| for (const [modelId, model] of Object.entries(providerData.models || {})) { |
| const cap: CapabilityEntry = { |
| tool_call: model.tool_call ?? null, |
| reasoning: model.reasoning ?? null, |
| attachment: model.attachment ?? null, |
| structured_output: model.structured_output ?? null, |
| temperature: model.temperature ?? null, |
| modalities_input: JSON.stringify(model.modalities?.input ?? []), |
| modalities_output: JSON.stringify(model.modalities?.output ?? []), |
| knowledge_cutoff: model.knowledge ?? null, |
| release_date: model.release_date ?? null, |
| last_updated: model.last_updated ?? null, |
| status: model.status ?? null, |
| family: model.family ?? null, |
| open_weights: model.open_weights ?? null, |
| limit_context: model.limit?.context ?? null, |
| limit_input: model.limit?.input ?? null, |
| limit_output: model.limit?.output ?? null, |
| interleaved_field: |
| typeof model.interleaved === "object" && model.interleaved?.field |
| ? model.interleaved.field |
| : model.interleaved === true |
| ? "reasoning_content" |
| : null, |
| }; |
|
|
| for (const omniProvider of omniRouteProviders) { |
| if (!result[omniProvider]) result[omniProvider] = {}; |
| result[omniProvider][modelId] = cap; |
| } |
| } |
| } |
|
|
| return result; |
| } |
|
|
| |
|
|
| function toRecord(value: unknown): Record<string, unknown> { |
| return value && typeof value === "object" ? (value as Record<string, unknown>) : {}; |
| } |
|
|
| |
| |
| |
| export function getModelsDevPricing(): PricingByProvider { |
| const db = getDbInstance(); |
| const rows = db |
| .prepare("SELECT key, value FROM key_value WHERE namespace = 'models_dev_pricing'") |
| .all(); |
| const synced: PricingByProvider = {}; |
| for (const row of rows) { |
| const record = toRecord(row); |
| const key = typeof record.key === "string" ? record.key : null; |
| const rawValue = typeof record.value === "string" ? record.value : null; |
| if (!key || rawValue === null) continue; |
| try { |
| synced[key] = JSON.parse(rawValue) as PricingModels; |
| } catch { |
| console.warn(`[MODELS_DEV] Corrupted pricing data for provider "${key}", skipping`); |
| } |
| } |
| return synced; |
| } |
|
|
| |
| |
| |
| export function saveModelsDevPricing(data: PricingByProvider): void { |
| const db = getDbInstance(); |
| const del = db.prepare("DELETE FROM key_value WHERE namespace = 'models_dev_pricing'"); |
| const insert = db.prepare( |
| "INSERT INTO key_value (namespace, key, value) VALUES ('models_dev_pricing', ?, ?)" |
| ); |
| const tx = db.transaction(() => { |
| del.run(); |
| for (const [provider, models] of Object.entries(data)) { |
| insert.run(provider, JSON.stringify(models)); |
| } |
| }); |
| tx(); |
| backupDbFile("pre-write"); |
| invalidateDbCache("pricing"); |
| } |
|
|
| |
| |
| |
| export function clearModelsDevPricing(): void { |
| const db = getDbInstance(); |
| db.prepare("DELETE FROM key_value WHERE namespace = 'models_dev_pricing'").run(); |
| backupDbFile("pre-write"); |
| invalidateDbCache("pricing"); |
| } |
|
|
| |
|
|
| |
| |
| |
| |
| export function ensureCapabilitiesTable(): void { |
| const db = getDbInstance(); |
| db.exec(` |
| CREATE TABLE IF NOT EXISTS model_capabilities ( |
| provider TEXT NOT NULL, |
| model_id TEXT NOT NULL, |
| tool_call BOOLEAN, |
| reasoning BOOLEAN, |
| attachment BOOLEAN, |
| structured_output BOOLEAN, |
| temperature BOOLEAN, |
| modalities_input TEXT, |
| modalities_output TEXT, |
| knowledge_cutoff TEXT, |
| release_date TEXT, |
| last_updated TEXT, |
| status TEXT, |
| family TEXT, |
| open_weights BOOLEAN, |
| limit_context INTEGER, |
| limit_input INTEGER, |
| limit_output INTEGER, |
| interleaved_field TEXT, |
| last_synced TEXT, |
| PRIMARY KEY (provider, model_id) |
| ) |
| `); |
| } |
|
|
| |
| |
| |
| export function getSyncedCapabilities( |
| provider?: string, |
| modelId?: string |
| ): Record<string, Record<string, CapabilityEntry>> { |
| const db = getDbInstance(); |
| ensureCapabilitiesTable(); |
|
|
| let query = "SELECT * FROM model_capabilities"; |
| const params: (string | number)[] = []; |
|
|
| if (provider) { |
| query += " WHERE provider = ?"; |
| params.push(provider); |
| if (modelId) { |
| query += " AND model_id = ?"; |
| params.push(modelId); |
| } |
| } |
|
|
| const rows = db.prepare(query).all(...params); |
| const result: Record<string, Record<string, CapabilityEntry>> = {}; |
|
|
| for (const row of rows) { |
| const record = toRecord(row); |
| const prov = typeof record.provider === "string" ? record.provider : null; |
| const mid = typeof record.model_id === "string" ? record.model_id : null; |
| if (!prov || !mid) continue; |
|
|
| if (!result[prov]) result[prov] = {}; |
| result[prov][mid] = { |
| tool_call: record.tool_call === 1 ? true : record.tool_call === 0 ? false : null, |
| reasoning: record.reasoning === 1 ? true : record.reasoning === 0 ? false : null, |
| attachment: record.attachment === 1 ? true : record.attachment === 0 ? false : null, |
| structured_output: |
| record.structured_output === 1 ? true : record.structured_output === 0 ? false : null, |
| temperature: record.temperature === 1 ? true : record.temperature === 0 ? false : null, |
| modalities_input: |
| typeof record.modalities_input === "string" ? record.modalities_input : "[]", |
| modalities_output: |
| typeof record.modalities_output === "string" ? record.modalities_output : "[]", |
| knowledge_cutoff: |
| typeof record.knowledge_cutoff === "string" ? record.knowledge_cutoff : null, |
| release_date: typeof record.release_date === "string" ? record.release_date : null, |
| last_updated: typeof record.last_updated === "string" ? record.last_updated : null, |
| status: typeof record.status === "string" ? record.status : null, |
| family: typeof record.family === "string" ? record.family : null, |
| open_weights: record.open_weights === 1 ? true : record.open_weights === 0 ? false : null, |
| limit_context: typeof record.limit_context === "number" ? record.limit_context : null, |
| limit_input: typeof record.limit_input === "number" ? record.limit_input : null, |
| limit_output: typeof record.limit_output === "number" ? record.limit_output : null, |
| interleaved_field: |
| typeof record.interleaved_field === "string" ? record.interleaved_field : null, |
| }; |
| } |
|
|
| return result; |
| } |
|
|
| |
| |
| |
| export function saveModelsDevCapabilities(data: CapabilitiesByProvider): void { |
| const db = getDbInstance(); |
| ensureCapabilitiesTable(); |
|
|
| const del = db.prepare("DELETE FROM model_capabilities"); |
| const insert = db.prepare(` |
| INSERT INTO model_capabilities ( |
| provider, model_id, tool_call, reasoning, attachment, structured_output, |
| temperature, modalities_input, modalities_output, knowledge_cutoff, |
| release_date, last_updated, status, family, open_weights, |
| limit_context, limit_input, limit_output, interleaved_field, last_synced |
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |
| `); |
|
|
| const now = new Date().toISOString(); |
| const tx = db.transaction(() => { |
| del.run(); |
| for (const [provider, models] of Object.entries(data)) { |
| for (const [modelId, cap] of Object.entries(models)) { |
| insert.run( |
| provider, |
| modelId, |
| cap.tool_call === null ? null : cap.tool_call ? 1 : 0, |
| cap.reasoning === null ? null : cap.reasoning ? 1 : 0, |
| cap.attachment === null ? null : cap.attachment ? 1 : 0, |
| cap.structured_output === null ? null : cap.structured_output ? 1 : 0, |
| cap.temperature === null ? null : cap.temperature ? 1 : 0, |
| cap.modalities_input, |
| cap.modalities_output, |
| cap.knowledge_cutoff, |
| cap.release_date, |
| cap.last_updated, |
| cap.status, |
| cap.family, |
| cap.open_weights === null ? null : cap.open_weights ? 1 : 0, |
| cap.limit_context, |
| cap.limit_input, |
| cap.limit_output, |
| cap.interleaved_field, |
| now |
| ); |
| } |
| } |
| }); |
| tx(); |
| backupDbFile("pre-write"); |
| } |
|
|
| |
| |
| |
| export function clearModelsDevCapabilities(): void { |
| const db = getDbInstance(); |
| ensureCapabilitiesTable(); |
| db.prepare("DELETE FROM model_capabilities").run(); |
| backupDbFile("pre-write"); |
| } |
|
|
| |
|
|
| |
| |
| |
| export async function syncModelsDev(opts?: { |
| dryRun?: boolean; |
| syncCapabilities?: boolean; |
| }): Promise<SyncResult> { |
| const dryRun = opts?.dryRun ?? false; |
| const syncCapabilities = opts?.syncCapabilities ?? true; |
|
|
| try { |
| const raw = await fetchModelsDev(); |
| const pricing = transformModelsDevToPricing(raw); |
| const capabilities = syncCapabilities ? transformModelsDevToCapabilities(raw) : {}; |
|
|
| const modelCount = Object.values(pricing).reduce( |
| (sum, models) => sum + Object.keys(models).length, |
| 0 |
| ); |
| const providerCount = Object.keys(pricing).length; |
| const capabilityCount = syncCapabilities |
| ? Object.values(capabilities).reduce((sum, models) => sum + Object.keys(models).length, 0) |
| : 0; |
|
|
| if (!dryRun) { |
| saveModelsDevPricing(pricing); |
| if (syncCapabilities) { |
| ensureCapabilitiesTable(); |
| saveModelsDevCapabilities(capabilities); |
| } |
| lastSyncTime = new Date().toISOString(); |
| lastSyncModelCount = modelCount; |
| lastSyncCapabilityCount = capabilityCount; |
| } |
|
|
| return { |
| success: true, |
| modelCount, |
| providerCount, |
| capabilityCount, |
| dryRun, |
| ...(dryRun ? { data: { pricing, capabilities } } : {}), |
| }; |
| } catch (err) { |
| const message = err instanceof Error ? err.message : String(err); |
| console.warn("[MODELS_DEV] Sync failed:", message); |
| return { |
| success: false, |
| modelCount: 0, |
| providerCount: 0, |
| capabilityCount: 0, |
| dryRun, |
| error: message, |
| }; |
| } |
| } |
|
|
| |
|
|
| |
| |
| |
| export function startPeriodicSync(intervalMs?: number): void { |
| if (syncTimer) return; |
|
|
| const interval = intervalMs ?? SYNC_INTERVAL_MS; |
| activeSyncIntervalMs = interval; |
| console.log(`[MODELS_DEV] Starting periodic sync every ${interval / 1000}s`); |
|
|
| |
| syncModelsDev() |
| .then((result) => { |
| if (result.success) { |
| console.log( |
| `[MODELS_DEV] Initial sync complete: ${result.modelCount} pricing entries, ${result.capabilityCount} capabilities from ${result.providerCount} providers` |
| ); |
| } |
| }) |
| .catch((err) => { |
| console.warn("[MODELS_DEV] Initial sync error:", err instanceof Error ? err.message : err); |
| }); |
|
|
| syncTimer = setInterval(() => { |
| syncModelsDev() |
| .then((result) => { |
| if (result.success) { |
| console.log(`[MODELS_DEV] Periodic sync complete: ${result.modelCount} pricing entries`); |
| } |
| }) |
| .catch((err) => { |
| console.warn("[MODELS_DEV] Periodic sync error:", err instanceof Error ? err.message : err); |
| }); |
| }, interval); |
|
|
| if (syncTimer && typeof syncTimer === "object" && "unref" in syncTimer) { |
| (syncTimer as { unref?: () => void }).unref?.(); |
| } |
| } |
|
|
| |
| |
| |
| export function stopPeriodicSync(): void { |
| if (syncTimer) { |
| clearInterval(syncTimer); |
| syncTimer = null; |
| console.log("[MODELS_DEV] Periodic sync stopped"); |
| } |
| } |
|
|
| |
| |
| |
| export function getSyncStatus(): SyncStatus { |
| |
| const enabled = syncTimer !== null; |
| return { |
| enabled, |
| lastSync: lastSyncTime, |
| lastSyncModelCount, |
| lastSyncCapabilityCount, |
| nextSync: |
| syncTimer && lastSyncTime |
| ? new Date(new Date(lastSyncTime).getTime() + activeSyncIntervalMs).toISOString() |
| : null, |
| intervalMs: activeSyncIntervalMs, |
| }; |
| } |
|
|
| |
|
|
| |
| |
| |
| export async function initModelsDevSync(): Promise<void> { |
| const { getSettings } = await import("./localDb"); |
| const settings = await getSettings(); |
|
|
| if (settings.modelsDevSyncEnabled !== true) { |
| console.log("[MODELS_DEV] Disabled (enable via Settings > AI)"); |
| return; |
| } |
|
|
| const interval = settings.modelsDevSyncInterval as number | undefined; |
| startPeriodicSync(interval); |
| } |
|
|
| |
| |
| |
| |
| export function getModelContextLimit(provider: string, modelId: string): number | null { |
| const caps = getSyncedCapabilities(provider, modelId); |
| return caps[provider]?.[modelId]?.limit_context ?? null; |
| } |
|
|