mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +00:00
Issue #1161. Sweeps 127 files to import from src/config/config-loader-facade.ts instead of unified-config-loader or utils/config-manager directly. WRITE callers (32 files): replaced raw saveUnifiedConfig / mutateUnifiedConfig / updateUnifiedConfig calls with the facade's cache-coherent wrappers saveConfig / mutateConfig / updateConfig. This fixes a latent stale-cache window where direct writes through the underlying loader bypassed the facade's memoization. READ callers (95 files): mechanical import-path migration only — function names unchanged because the facade re-exports them. No behavior change. Also updated: - tests/unit/utils/browser/browser-setup.test.ts (DI interface rename) - src/management/checks/image-analysis-check.ts (dynamic import rename) - src/web-server/health-service.ts (dynamic require rename) - src/ccs.ts (path prefix fix from sweep script) After sweep: zero raw write callers remain outside src/config/. Direct imports of config-manager remain only for symbols not in the facade (getConfigPath, getCcsDirSource, etc). Behavior unchanged; full suite passes 1824/1824. Out of scope: switching loadOrCreateUnifiedConfig() callers to getCachedConfig() — needs per-callsite cache-safety analysis. Tracked as follow-up. Refs #1161
124 lines
3.4 KiB
TypeScript
124 lines
3.4 KiB
TypeScript
/**
|
|
* OpenRouter Model Catalog Fetcher
|
|
* Fetches model list from OpenRouter API for CLI use
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { getCcsDir } from '../../config/config-loader-facade';
|
|
|
|
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/models';
|
|
function getCacheFile() {
|
|
return path.join(getCcsDir(), 'openrouter-models-cache.json');
|
|
}
|
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
|
|
export interface OpenRouterModel {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
context_length: number;
|
|
pricing: {
|
|
prompt: string;
|
|
completion: string;
|
|
};
|
|
}
|
|
|
|
interface CacheData {
|
|
models: OpenRouterModel[];
|
|
fetchedAt: number;
|
|
}
|
|
|
|
/** Check if cached data is valid */
|
|
function getCachedModels(): OpenRouterModel[] | null {
|
|
try {
|
|
const cacheFile = getCacheFile();
|
|
if (!fs.existsSync(cacheFile)) return null;
|
|
const data = JSON.parse(fs.readFileSync(cacheFile, 'utf8')) as CacheData;
|
|
if (Date.now() - data.fetchedAt > CACHE_TTL_MS) return null;
|
|
return data.models;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Save models to cache */
|
|
function setCachedModels(models: OpenRouterModel[]): void {
|
|
try {
|
|
const cacheFile = getCacheFile();
|
|
const dir = path.dirname(cacheFile);
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(
|
|
cacheFile,
|
|
JSON.stringify({
|
|
models,
|
|
fetchedAt: Date.now(),
|
|
})
|
|
);
|
|
} catch {
|
|
// Ignore cache write errors
|
|
}
|
|
}
|
|
|
|
/** Fetch models from OpenRouter API */
|
|
export async function fetchOpenRouterModels(): Promise<OpenRouterModel[]> {
|
|
// Try cache first
|
|
const cached = getCachedModels();
|
|
if (cached) return cached;
|
|
|
|
// Fetch from API
|
|
const response = await fetch(OPENROUTER_API_URL);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch OpenRouter models: ${response.status}`);
|
|
}
|
|
|
|
const data = (await response.json()) as { data: OpenRouterModel[] };
|
|
const models = data.data.map((m) => ({
|
|
id: m.id,
|
|
name: m.name,
|
|
description: m.description,
|
|
context_length: m.context_length,
|
|
pricing: m.pricing,
|
|
}));
|
|
|
|
// Cache for next time
|
|
setCachedModels(models);
|
|
|
|
return models;
|
|
}
|
|
|
|
/** Format price per token to per million */
|
|
export function formatPrice(perToken: string): string {
|
|
const value = parseFloat(perToken);
|
|
if (isNaN(value) || value === 0) return 'Free';
|
|
const perMillion = value * 1_000_000;
|
|
if (perMillion < 0.01) return '<$0.01';
|
|
if (perMillion < 1) return `$${perMillion.toFixed(2)}`;
|
|
return `$${perMillion.toFixed(perMillion < 10 ? 2 : 0)}`;
|
|
}
|
|
|
|
/** Format pricing pair */
|
|
export function formatPricingPair(pricing: { prompt: string; completion: string }): string {
|
|
return `${formatPrice(pricing.prompt)}/${formatPrice(pricing.completion)}`;
|
|
}
|
|
|
|
/** Format context length */
|
|
export function formatContext(length: number): string {
|
|
if (length >= 1_000_000) return `${(length / 1_000_000).toFixed(1)}M`;
|
|
return `${Math.round(length / 1_000)}K`;
|
|
}
|
|
|
|
/** Search models */
|
|
export function searchModels(models: OpenRouterModel[], query: string): OpenRouterModel[] {
|
|
if (!query.trim()) return models.slice(0, 20); // Show first 20 if no query
|
|
const q = query.toLowerCase();
|
|
return models
|
|
.filter((m) => m.id.toLowerCase().includes(q) || m.name.toLowerCase().includes(q))
|
|
.slice(0, 20); // Limit to 20 results
|
|
}
|
|
|
|
/** Check if URL is OpenRouter */
|
|
export function isOpenRouterUrl(url: string): boolean {
|
|
return url.toLowerCase().includes('openrouter.ai');
|
|
}
|