Files
ccs/src/api/services/openrouter-catalog.ts
T
Tam Nhu Tran 4f6e61739c refactor(config): adopt config-loader-facade across the codebase
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
2026-05-03 01:42:53 -04:00

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');
}