feat: integrate models.dev pricing metadata

This commit is contained in:
Tam Nhu Tran committed 2026-04-28 14:50:26 -04:00
1 parent 55ddb69104
commit a38c1a75ba
14 files changed
+851 -88

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
import * as fs from 'fs';
import * as path from 'path';
import { getCcsDir } from '../../utils/config-manager';
import type { ModelsDevCacheData, ModelsDevProvider, ModelsDevRegistry } from './types';
export const MODELS_DEV_API_URL = 'https://models.dev/api.json';
const CACHE_FILE_NAME = 'models-dev-registry-cache.json';
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
const LIVE_FETCH_TIMEOUT_MS = 3000;
export interface RegistryCacheReadOptions {
allowStale?: boolean;
now?: number;
}
export interface RegistryRefreshOptions {
force?: boolean;
fetchImpl?: typeof fetch;
now?: () => number;
}
function getCacheFilePath(): string {
return path.join(getCcsDir(), CACHE_FILE_NAME);
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function normalizeRegistryPayload(payload: unknown): ModelsDevRegistry | null {
if (!isPlainObject(payload)) return null;
const providers: ModelsDevRegistry = {};
for (const [key, value] of Object.entries(payload)) {
if (!isPlainObject(value)) continue;
const id = typeof value.id === 'string' && value.id.trim() ? value.id.trim() : key;
const models = isPlainObject(value.models)
? (value.models as NonNullable<ModelsDevProvider['models']>)
: undefined;
if (!models || Object.keys(models).length === 0) continue;
providers[id] = {
...(value as ModelsDevProvider),
id,
models,
};
}
return Object.keys(providers).length > 0 ? providers : null;
}
function normalizeCachePayload(payload: unknown): ModelsDevCacheData | null {
if (!isPlainObject(payload)) return null;
if (payload.version !== 1 || typeof payload.fetchedAt !== 'number') return null;
const providers = normalizeRegistryPayload(payload.providers);
return providers ? { version: 1, fetchedAt: payload.fetchedAt, providers } : null;
}
export function getCachedModelsDevRegistry(
options: RegistryCacheReadOptions = {}
): ModelsDevRegistry | null {
try {
const filePath = getCacheFilePath();
if (!fs.existsSync(filePath)) return null;
const cache = normalizeCachePayload(JSON.parse(fs.readFileSync(filePath, 'utf8')));
if (!cache) return null;
const now = options.now ?? Date.now();
if (!options.allowStale && now - cache.fetchedAt > CACHE_TTL_MS) return null;
return cache.providers;
} catch {
return null;
}
}
export function setCachedModelsDevRegistry(
providers: ModelsDevRegistry,
fetchedAt = Date.now()
): void {
try {
const filePath = getCacheFilePath();
fs.mkdirSync(path.dirname(filePath), { recursive: true });
const cache: ModelsDevCacheData = { version: 1, fetchedAt, providers };
fs.writeFileSync(filePath, JSON.stringify(cache));
} catch {
// Best-effort cache writes must not break analytics.
}
}
export function clearModelsDevRegistryCache(): boolean {
try {
const filePath = getCacheFilePath();
if (!fs.existsSync(filePath)) return false;
fs.unlinkSync(filePath);
return true;
} catch {
return false;
}
}
export async function refreshModelsDevRegistry(
options: RegistryRefreshOptions = {}
): Promise<ModelsDevRegistry | null> {
const now = options.now ?? (() => Date.now());
if (!options.force) {
const fresh = getCachedModelsDevRegistry({ allowStale: false, now: now() });
if (fresh) return fresh;
}
const fetchImpl = options.fetchImpl ?? fetch;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), LIVE_FETCH_TIMEOUT_MS);
try {
const response = await fetchImpl(MODELS_DEV_API_URL, {
signal: controller.signal,
headers: { Accept: 'application/json' },
});
if (!response.ok) return getCachedModelsDevRegistry({ allowStale: true });
const providers = normalizeRegistryPayload(await response.json());
if (!providers) return getCachedModelsDevRegistry({ allowStale: true });
setCachedModelsDevRegistry(providers, now());
return providers;
} catch {
return getCachedModelsDevRegistry({ allowStale: true });
} finally {
clearTimeout(timeoutId);
}
}