Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 51 additions & 14 deletions src/openrouter/model-sync/synccheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -150,8 +149,12 @@ export async function runSyncCheck(apiKey: string): Promise<SyncCheckResult> {
}
}

/** 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) {
Expand Down Expand Up @@ -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 <alias> after /syncall.\n');

let currentFamily = '';
// Group by family
const byFamily = new Map<string, typeof result.newFamilyModels>();
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;
}
67 changes: 57 additions & 10 deletions src/openrouter/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, ModelInfo[]>();
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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 <model-alias> 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 <alias> to switch`);
}
}

lines.push('\n━━━ Legend ━━━');
Expand Down
Loading