/**
* OpenRouter API Client
*
* Fetches current models from OpenRouter and filters for:
* - Tool-capable models (supports function calling)
* - Recent models (released within 180 days)
* - AI Gateway supported providers
*
* Results are cached in KV for 24 hours.
*/
const OPENROUTER_API = 'https://openrouter.ai/api/v1/models';
const CACHE_KEY = 'openrouter:models';
const CACHE_TTL = 24 * 60 * 60; // 24 hours
const RECENCY_DAYS = 180;
/**
* OpenRouter model response shape
*/
interface OpenRouterModel {
id: string;
name: string;
created: number;
context_length: number;
supported_parameters?: string[];
pricing: {
prompt: string;
completion: string;
};
}
/**
* Filtered model for our use
*/
export interface FilteredModel {
id: string;
name: string;
provider: string;
gatewayProvider: string;
contextLength: number;
created: string;
pricing: {
prompt: number; // per 1M tokens
completion: number; // per 1M tokens
};
}
/**
* Cached response shape
*/
export interface ModelsCache {
models: FilteredModel[];
providers: string[];
cachedAt: string;
}
/**
* OpenRouter provider ID -> AI Gateway provider ID
* Some providers have different names in AI Gateway
* See: https://developers.cloudflare.com/ai-gateway/usage/chat-completion/
*/
export const PROVIDER_MAP: Record<string, string> = {
'openai': 'openai',
'anthropic': 'anthropic',
'google': 'google-ai-studio',
'groq': 'groq',
'mistralai': 'mistral',
'deepseek': 'deepseek',
'cohere': 'cohere',
'x-ai': 'grok', // AI Gateway uses "grok" for xAI
};
/**
* Display names for providers
*/
export const PROVIDER_DISPLAY: Record<string, string> = {
'openai': 'OpenAI',
'anthropic': 'Anthropic',
'google-ai-studio': 'Google AI Studio',
'groq': 'Groq',
'mistral': 'Mistral AI',
'deepseek': 'DeepSeek',
'cohere': 'Cohere',
'grok': 'xAI (Grok)', // AI Gateway uses "grok" as provider ID
'cloudflare': 'Workers AI',
};
/**
* Provider sort order (preferred providers first)
*/
const PROVIDER_ORDER: string[] = [
'anthropic',
'google',
'openai',
'groq',
'mistralai',
'deepseek',
'cohere',
'x-ai',
];
/**
* Fetch and filter models from OpenRouter
*/
export async function fetchModels(
kv: KVNamespace,
forceRefresh = false
): Promise<ModelsCache> {
// Check cache first (unless force refresh)
if (!forceRefresh) {
const cached = await kv.get(CACHE_KEY, 'json') as ModelsCache | null;
if (cached) {
return { ...cached, cached: true } as ModelsCache & { cached: boolean };
}
}
// Fetch from OpenRouter
const response = await fetch(OPENROUTER_API, {
headers: {
'User-Agent': 'MCP-Server-Template/1.0',
},
});
if (!response.ok) {
throw new Error(`OpenRouter API error: ${response.status}`);
}
const data = await response.json() as { data: OpenRouterModel[] };
// Filter and transform
const cutoff = Date.now() / 1000 - (RECENCY_DAYS * 24 * 60 * 60);
const gatewayProviders = Object.keys(PROVIDER_MAP);
const models = data.data
.filter(m => {
const provider = m.id.split('/')[0];
const hasTools = m.supported_parameters?.includes('tools') ||
m.supported_parameters?.includes('tool_choice');
const isRecent = m.created > cutoff;
const isGatewayProvider = gatewayProviders.includes(provider);
return hasTools && isRecent && isGatewayProvider;
})
.map(m => {
const openrouterProvider = m.id.split('/')[0];
const gatewayProvider = PROVIDER_MAP[openrouterProvider] || openrouterProvider;
return {
id: m.id,
name: m.name,
provider: openrouterProvider,
gatewayProvider,
contextLength: m.context_length,
created: new Date(m.created * 1000).toISOString().split('T')[0],
pricing: {
// Convert from per-token to per-1M tokens for readability
prompt: parseFloat(m.pricing.prompt) * 1_000_000,
completion: parseFloat(m.pricing.completion) * 1_000_000,
},
};
})
// Sort by provider order, then by name
.sort((a, b) => {
const aOrder = PROVIDER_ORDER.indexOf(a.provider);
const bOrder = PROVIDER_ORDER.indexOf(b.provider);
const aIdx = aOrder === -1 ? 999 : aOrder;
const bIdx = bOrder === -1 ? 999 : bOrder;
if (aIdx !== bIdx) return aIdx - bIdx;
return a.name.localeCompare(b.name);
});
// Get unique providers (in sort order)
const providers = [...new Set(models.map(m => m.gatewayProvider))];
const result: ModelsCache = {
models,
providers,
cachedAt: new Date().toISOString(),
};
// Cache result
await kv.put(CACHE_KEY, JSON.stringify(result), {
expirationTtl: CACHE_TTL,
});
return result;
}
/**
* Get models for a specific provider
*/
export async function getModelsForProvider(
kv: KVNamespace,
provider: string,
forceRefresh = false
): Promise<FilteredModel[]> {
const { models } = await fetchModels(kv, forceRefresh);
return models.filter(m => m.gatewayProvider === provider);
}
/**
* Invalidate the models cache
*/
export async function invalidateCache(kv: KVNamespace): Promise<void> {
await kv.delete(CACHE_KEY);
}