import { logger } from '@/lib/utils'; export interface ModelArchitecture { input_modalities: string[]; output_modalities: string[]; tokenizer: string; instruct_type: string | null; } export interface ModelPricing { prompt: string; completion: string; request: string; image: string; web_search: string; internal_reasoning: string; input_cache_read: string; input_cache_write: string; } export interface TopProvider { context_length: number; max_completion_tokens: number; is_moderated: boolean; } export interface OpenRouterModel { id: string; canonical_slug: string; name: string; created: number; description: string; context_length: number; architecture: ModelArchitecture; pricing: ModelPricing; top_provider: TopProvider; per_request_limits: any | null; supported_parameters: string[]; } export interface ModelsResponse { data: OpenRouterModel[]; } export async function fetchAvailableModels(): Promise { try { // Request both text and image output modalities. The default (no param) // response omits image-only generation models (FLUX, Seedream, Grok Imagine, // etc.) because they don't output text — only text+image multimodal models // (Gemini, GPT Image) come back. Asking for 'text,image' includes both; // per-slot filtering (matchesSlot) narrows to the right set downstream. const response = await fetch('https://openrouter.ai/api/v1/models?output_modalities=text,image'); if (!response.ok) { throw new Error(`Failed to fetch models: ${response.statusText}`); } const data: ModelsResponse = await response.json(); // Keep models that output text (agent/chat) OR image (image generation). // Per-slot filtering downstream (matchesSlot) narrows to the right set; // filtering on 'text' alone here would drop image-only models such as // FLUX, Seedream, and Grok Imagine before the imageGen slot ever sees them. const filteredModels = data.data.filter(model => model.architecture.output_modalities.includes('text') || model.architecture.output_modalities.includes('image') ); return filteredModels.sort((a, b) => { const popularModels = ['gpt-4', 'claude', 'deepseek', 'qwen']; const aIsPopular = popularModels.some(p => a.id.toLowerCase().includes(p)); const bIsPopular = popularModels.some(p => b.id.toLowerCase().includes(p)); if (aIsPopular && !bIsPopular) return -1; if (!aIsPopular && bIsPopular) return 1; return b.created - a.created; }); } catch (error) { logger.error('Error fetching models:', error); return getDefaultModels(); } } /** * Format model price with appropriate precision * @param price Price per million tokens * @param perK If true, show price per 1K tokens (default), otherwise per 1M */ export function formatModelPrice(price: number | undefined, perK: boolean = true): string { if (price === undefined || price === null) return ''; const displayPrice = perK ? price / 1000 : price; if (displayPrice === 0) return 'free'; if (displayPrice < 0.0001) { return `$${displayPrice.toFixed(5).replace(/\.?0+$/, '')}`; } else if (displayPrice < 0.001) { return `$${displayPrice.toFixed(4).replace(/\.?0+$/, '')}`; } else if (displayPrice < 0.01) { return `$${displayPrice.toFixed(3).replace(/\.?0+$/, '')}`; } else if (displayPrice < 0.1) { return `$${displayPrice.toFixed(3).replace(/\.?0+$/, '')}`; } else if (displayPrice < 1) { return `$${displayPrice.toFixed(2).replace(/\.?0+$/, '')}`; } else { return `$${displayPrice.toFixed(2)}`; } } export function getDefaultModels(): OpenRouterModel[] { return [ { id: 'deepseek/deepseek-chat', canonical_slug: 'deepseek-chat', name: 'DeepSeek Chat', created: Date.now(), description: 'DeepSeek Chat - Fast and capable model for general tasks', context_length: 64000, architecture: { input_modalities: ['text'], output_modalities: ['text'], tokenizer: 'cl100k_base', instruct_type: 'deepseek' }, pricing: { prompt: '0.00014', completion: '0.00028', request: '0', image: '0', web_search: '0', internal_reasoning: '0', input_cache_read: '0', input_cache_write: '0' }, top_provider: { context_length: 64000, max_completion_tokens: 8192, is_moderated: false }, per_request_limits: null, supported_parameters: ['tools', 'tool_choice', 'temperature', 'max_tokens'] }, { id: 'qwen/qwen-2.5-coder-32b-instruct', canonical_slug: 'qwen-2.5-coder-32b-instruct', name: 'Qwen 2.5 Coder 32B', created: Date.now(), description: 'Qwen 2.5 Coder - Specialized for code generation', context_length: 32768, architecture: { input_modalities: ['text'], output_modalities: ['text'], tokenizer: 'cl100k_base', instruct_type: 'qwen' }, pricing: { prompt: '0.00018', completion: '0.00018', request: '0', image: '0', web_search: '0', internal_reasoning: '0', input_cache_read: '0', input_cache_write: '0' }, top_provider: { context_length: 32768, max_completion_tokens: 8192, is_moderated: false }, per_request_limits: null, supported_parameters: ['tools', 'tool_choice', 'temperature', 'max_tokens'] }, { id: 'openai/gpt-4o', canonical_slug: 'gpt-4o', name: 'GPT-4o', created: Date.now(), description: 'OpenAI GPT-4o - Multimodal model with vision capabilities', context_length: 128000, architecture: { input_modalities: ['text', 'image'], output_modalities: ['text'], tokenizer: 'cl100k_base', instruct_type: 'openai' }, pricing: { prompt: '0.0025', completion: '0.01', request: '0', image: '0.00765', web_search: '0', internal_reasoning: '0', input_cache_read: '0.00125', input_cache_write: '0.0025' }, top_provider: { context_length: 128000, max_completion_tokens: 16384, is_moderated: true }, per_request_limits: null, supported_parameters: ['tools', 'tool_choice', 'temperature', 'max_tokens', 'response_format'] }, { id: 'anthropic/claude-3.5-sonnet', canonical_slug: 'claude-3.5-sonnet', name: 'Claude 3.5 Sonnet', created: Date.now(), description: 'Anthropic Claude 3.5 Sonnet - Advanced reasoning and coding', context_length: 200000, architecture: { input_modalities: ['text', 'image'], output_modalities: ['text'], tokenizer: 'claude', instruct_type: 'anthropic' }, pricing: { prompt: '0.003', completion: '0.015', request: '0', image: '0.0048', web_search: '0', internal_reasoning: '0', input_cache_read: '0.0003', input_cache_write: '0.00375' }, top_provider: { context_length: 200000, max_completion_tokens: 8192, is_moderated: false }, per_request_limits: null, supported_parameters: ['tools', 'tool_choice', 'temperature', 'max_tokens'] } ]; } export function getModelDisplayName(model: OpenRouterModel): string { if (model.name.length > 30) { const parts = model.id.split('/'); const provider = parts[0]; const modelName = parts[1]; const versionMatch = modelName.match(/(\d+[\.\d]*)/); const version = versionMatch ? versionMatch[1] : ''; if (provider === 'openai') { if (modelName.includes('gpt-4o')) return 'GPT-4o'; if (modelName.includes('gpt-4')) return `GPT-4${version ? ` ${version}` : ''}`; if (modelName.includes('gpt-3.5')) return 'GPT-3.5 Turbo'; } if (provider === 'anthropic') { if (modelName.includes('claude-3.5-sonnet')) return 'Claude 3.5 Sonnet'; if (modelName.includes('claude-3.5-haiku')) return 'Claude 3.5 Haiku'; if (modelName.includes('claude-3-opus')) return 'Claude 3 Opus'; } if (provider === 'deepseek') { if (modelName.includes('chat')) return 'DeepSeek Chat'; if (modelName.includes('coder')) return `DeepSeek Coder${version ? ` ${version}` : ''}`; if (modelName.includes('reasoner')) return 'DeepSeek Reasoner'; } if (provider === 'qwen' && modelName.includes('coder')) { const sizeMatch = modelName.match(/(\d+b)/i); const size = sizeMatch ? ` ${sizeMatch[1].toUpperCase()}` : ''; return `Qwen Coder${size}`; } } return model.name; }