diff --git a/src/openrouter/model-sync/synccheck.ts b/src/openrouter/model-sync/synccheck.ts index 0f7b45282..ec17a1135 100644 --- a/src/openrouter/model-sync/synccheck.ts +++ b/src/openrouter/model-sync/synccheck.ts @@ -7,8 +7,7 @@ * 3. Pricing changes for curated models */ -import { MODELS } from '../models'; -import type { ModelInfo } from '../models'; +import { MODELS, getAutoSyncedByModelId } from '../models'; import type { OpenRouterApiModel } from './types'; import { fetchOpenRouterModels } from './sync'; import { formatCostString } from './capabilities'; @@ -150,8 +149,12 @@ export async function runSyncCheck(apiKey: string): Promise { } } +/** Max models to show in detail per family before collapsing to summary. */ +const MAX_PER_FAMILY = 4; + /** * Format sync check results for Telegram display. + * Concise output: highlights actionable items, collapses older models. */ export function formatSyncCheckMessage(result: SyncCheckResult): string { if (!result.success) { @@ -183,28 +186,62 @@ export function formatSyncCheckMessage(result: SyncCheckResult): string { lines.push(`✅ ${ok.length} curated models OK`); - // New family models + // Family models — grouped, with auto-sync status and collapse for older ones if (result.newFamilyModels.length > 0) { lines.push(''); - lines.push('━━━ New models from tracked families ━━━'); + lines.push('━━━ Not yet curated (tracked families) ━━━'); + lines.push('Models below are usable via /use after /syncall.\n'); - let currentFamily = ''; + // Group by family + const byFamily = new Map(); for (const m of result.newFamilyModels) { - if (m.family !== currentFamily) { - currentFamily = m.family; - lines.push(`\n📦 ${currentFamily}:`); + if (!byFamily.has(m.family)) byFamily.set(m.family, []); + byFamily.get(m.family)!.push(m); + } + + for (const [family, models] of byFamily) { + // Sort by cost descending (flagship first) + models.sort((a, b) => { + const costA = parseSyncCost(a.cost); + const costB = parseSyncCost(b.cost); + return costB - costA; + }); + + lines.push(`📦 ${family} (${models.length}):`); + + // Show top models in detail + const shown = models.slice(0, MAX_PER_FAMILY); + const collapsed = models.length - shown.length; + + for (const m of shown) { + const ctx = m.contextLength >= 1048576 + ? `${Math.round(m.contextLength / 1048576)}M` + : `${Math.round(m.contextLength / 1024)}K`; + const synced = getAutoSyncedByModelId(m.id); + const aliasHint = synced ? ` → /${synced.alias}` : ''; + lines.push(` ${m.name} — ${m.cost} (${ctx} ctx)${aliasHint}`); } - const ctx = m.contextLength >= 1048576 - ? `${Math.round(m.contextLength / 1048576)}M` - : `${Math.round(m.contextLength / 1024)}K`; - lines.push(` ${m.name} — ${m.cost} (${ctx} ctx)`); - lines.push(` id: ${m.id}`); + + if (collapsed > 0) { + lines.push(` +${collapsed} older/variant models`); + } + lines.push(''); } } else { lines.push('\n📦 No new models from tracked families'); } - lines.push(`\n⚡ ${result.durationMs}ms — ${result.totalLiveModels} live models checked`); + lines.push(`⚡ ${result.durationMs}ms — ${result.totalLiveModels} live models checked`); return lines.join('\n'); } + +/** + * Parse cost string for sorting (higher cost = more flagship). + */ +function parseSyncCost(cost: string): number { + if (cost === 'FREE' || cost.includes('FREE')) return 0; + const match = cost.match(/\$([0-9.]+)\/\$([0-9.]+)/); + if (match) return (parseFloat(match[1]) + parseFloat(match[2])) / 2; + return 0; +} diff --git a/src/openrouter/models.ts b/src/openrouter/models.ts index 3dc72adb7..d9c3634cb 100644 --- a/src/openrouter/models.ts +++ b/src/openrouter/models.ts @@ -679,6 +679,39 @@ export function getAutoSyncedModelCount(): number { return Object.keys(AUTO_SYNCED_MODELS).length; } +/** Major providers whose auto-synced models are highlighted in /models and /synccheck. */ +const NOTABLE_PROVIDERS = ['anthropic', 'google', 'openai', 'deepseek', 'x-ai', 'meta-llama', 'mistralai']; + +/** + * Get notable auto-synced models for display in /models. + * Picks top 2 per major provider (highest cost = flagship), capped at 15. + */ +export function getNotableAutoSynced(): ModelInfo[] { + const byProvider = new Map(); + for (const m of Object.values(AUTO_SYNCED_MODELS)) { + const provider = m.id.split('/')[0]; + if (!NOTABLE_PROVIDERS.includes(provider)) continue; + if (!byProvider.has(provider)) byProvider.set(provider, []); + byProvider.get(provider)!.push(m); + } + + const notable: ModelInfo[] = []; + for (const models of byProvider.values()) { + models.sort((a, b) => parseCostForSort(b.cost) - parseCostForSort(a.cost)); + notable.push(...models.slice(0, 2)); + } + + notable.sort((a, b) => parseCostForSort(b.cost) - parseCostForSort(a.cost)); + return notable.slice(0, 15); +} + +/** + * Get auto-synced model by OpenRouter model ID (for synccheck cross-referencing). + */ +export function getAutoSyncedByModelId(modelId: string): ModelInfo | undefined { + return Object.values(AUTO_SYNCED_MODELS).find(m => m.id === modelId); +} + /** * Get all models merged: curated < auto-synced < dynamic (dynamic wins on conflict). * Excludes blocked models. @@ -920,13 +953,17 @@ export function formatModelsList(): string { const lines: string[] = ['📋 Model Catalog — sorted by value\n']; const all = Object.values(getAllModels()); - const free = all.filter(m => m.isFree && !m.isImageGen && !m.provider); - const imageGen = all.filter(m => m.isImageGen); - const paid = all.filter(m => !m.isFree && !m.isImageGen && !m.provider); - const direct = all.filter(m => m.provider && m.provider !== 'openrouter'); - - const freeCurated = free.filter(m => isCuratedModel(m.alias)); - const freeSynced = free.filter(m => !isCuratedModel(m.alias)); + // Tier sections show curated + dynamic only (auto-synced get their own section below) + const curated = all.filter(m => isCuratedModel(m.alias)); + const free = curated.filter(m => m.isFree && !m.isImageGen && !m.provider); + const imageGen = curated.filter(m => m.isImageGen); + const paid = curated.filter(m => !m.isFree && !m.isImageGen && !m.provider); + const direct = curated.filter(m => m.provider && m.provider !== 'openrouter'); + + // Dynamic (from /syncmodels) free models shown separately + const dynamicFree = all.filter(m => m.isFree && !m.isImageGen && !m.provider && !isCuratedModel(m.alias) && !isAutoSyncedModel(m.alias)); + const freeCurated = free; + const freeSynced = dynamicFree; const sortByCost = (a: ModelInfo, b: ModelInfo) => parseCostForSort(a.cost) - parseCostForSort(b.cost); paid.sort(sortByCost); @@ -991,11 +1028,21 @@ export function formatModelsList(): string { } } - // Auto-synced models summary (not listed individually — too many) + // Auto-synced models — show notable highlights + summary count const autoSyncedCount = getAutoSyncedModelCount(); if (autoSyncedCount > 0) { - lines.push(`\n🌐 +${autoSyncedCount} more models auto-synced from OpenRouter`); - lines.push(' Use /use to switch — /syncall to refresh'); + const notable = getNotableAutoSynced(); + if (notable.length > 0) { + lines.push('\n🌐 AUTO-SYNCED HIGHLIGHTS:'); + for (const m of notable) { + const features = [m.supportsVision && '👁️', m.supportsTools && '🔧'].filter(Boolean).join(''); + lines.push(` /${m.alias} — ${m.name} ${features}\n ${m.cost}`); + } + } + const remaining = autoSyncedCount - notable.length; + if (remaining > 0) { + lines.push(`\n +${remaining} more auto-synced — /use to switch`); + } } lines.push('\n━━━ Legend ━━━');