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
2 changes: 1 addition & 1 deletion client/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=llm-hub-20260616" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LLM-Hub Pro Max · Unified AI Router</title>
<script type="module" crossorigin src="/assets/index-BbRuEJun.js"></script>
<script type="module" crossorigin src="/assets/index-BC7vwQDA.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-gKqPbOvw.css">
</head>
<body>
Expand Down
17 changes: 16 additions & 1 deletion client/src/pages/ModelStatusPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface DiscoverResult {

export default function ModelStatusPage() {
const [filter, setFilter] = useState<string>('all')
const [showDisabled, setShowDisabled] = useState(false)

const { data, isLoading, isError, refetch, isRefetching, error } = useQuery<{
models: ModelAvailability[]
Expand All @@ -40,7 +41,11 @@ export default function ModelStatusPage() {
queryFn: () => apiFetch('/api/model-availability'),
})

const models = data?.models ?? []
const allModels = data?.models ?? []
// Disabled models cannot be routed to and are never live-checked, so counting
// them as "unknown" only inflates the numbers. Hidden unless asked for.
const disabledCount = allModels.filter(m => !m.enabled).length
const models = showDisabled ? allModels : allModels.filter(m => m.enabled)

const checkMutation = useMutation({
mutationFn: () => apiFetch('/api/model-availability/check', { method: 'POST' }),
Expand Down Expand Up @@ -202,6 +207,16 @@ export default function ModelStatusPage() {
<Rocket className={`size-3.5 mr-1.5 ${discoverMutation.isPending ? 'animate-pulse' : ''}`} />
Discover new models
</Button>
{disabledCount > 0 && (
<Button
variant="outline"
size="sm"
onClick={() => setShowDisabled(v => !v)}
title="Disabled models cannot be routed to and are never live-checked"
>
{showDisabled ? `Hide disabled (${disabledCount})` : `Show disabled (${disabledCount})`}
</Button>
)}
{filter !== 'all' && (
<Badge variant="secondary" className="cursor-pointer" onClick={() => setFilter('all')}>
Showing: {filter} — click to clear
Expand Down
24 changes: 24 additions & 0 deletions server/dist/db/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
import Database from 'better-sqlite3';
export declare function getDb(): Database.Database;
export declare function initDb(dbPath?: string): Promise<Database.Database>;
/**
* One-time cleanup: remove the BazaarLink rows imported before the scout's
* chat filter existed.
*
* On 2026-07-20 the scout pulled BazaarLink's entire ~260-model catalog in
* batches of MAX_DISCOVERED_PER_PLATFORM, including ~245 text-to-image and
* text-to-video models (wan2.1-t2i, happyhorse, Z-Image-Turbo) and paid chat
* models that can never serve /v1/chat/completions. They landed disabled — so
* nothing could route to them — but they clutter the catalog and inflate the
* "unknown / never scanned" counters on Model Status.
*
* isBazaarlinkFreeChatEntry now rejects all of them at discovery (verified
* against the live catalog: 2 of 260 pass), so this only clears the historical
* residue. Scoped deliberately narrowly:
* - platform bazaarlink only (other providers import catalogs by design)
* - disabled only (never touch something in use)
* - auto-discovered only (seeded/curated rows have no discovery_source)
* - discovered before the cutoff, so a later legitimate discovery that an
* operator chooses to disable is never swept up
*
* fallback_config is ON DELETE NO ACTION and must be cleared first;
* model_availability / model_capabilities / model_runtime_health cascade.
*/
export declare function purgeLegacyBazaarlinkDiscoveries(db: Database.Database): void;
export declare function getUnifiedApiKey(): string;
export declare function regenerateUnifiedKey(): string;
//# sourceMappingURL=index.d.ts.map
2 changes: 1 addition & 1 deletion server/dist/db/index.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions server/dist/db/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion server/dist/db/index.js.map

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions server/src/__tests__/db/purge-legacy-discoveries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import { initDb, getDb } from '../../db/index.js';
import { purgeLegacyBazaarlinkDiscoveries } from '../../db/index.js';

function addModel(platform: string, modelId: string, enabled: number): number {
return getDb().prepare(`
INSERT INTO models (platform, model_id, display_name, intelligence_rank, speed_rank, size_label, enabled, is_free)
VALUES (?, ?, ?, 99, 99, 'Medium', ?, 1)
`).run(platform, modelId, modelId, enabled).lastInsertRowid as number;
}

function markDiscovered(modelDbId: number, discoverySource: string) {
getDb().prepare(`
INSERT INTO model_availability (model_db_id, status, discovery_source, free_tier_confirmed)
VALUES (?, 'unknown', ?, 0)
`).run(modelDbId, discoverySource);
}

const OLD = 'model-scout:2026-07-20T20:53:52.429Z'; // the bad pre-filter import
const NEW = 'model-scout:2026-08-01T10:00:00.000Z'; // a legitimate later discovery

describe('purgeLegacyBazaarlinkDiscoveries', () => {
beforeAll(() => {
process.env.ENCRYPTION_KEY = '0'.repeat(64);
initDb(':memory:');
});

beforeEach(() => {
const db = getDb();
// fallback_config is NO ACTION, so clear it before removing the models.
db.prepare("DELETE FROM fallback_config WHERE model_db_id IN (SELECT id FROM models WHERE model_id LIKE 'purge-%')").run();
db.prepare("DELETE FROM models WHERE model_id LIKE 'purge-%'").run();
});

it('removes the disabled rows imported before the scout filter existed', () => {
const db = getDb();
const junk = addModel('bazaarlink', 'purge-junk-image', 0);
markDiscovered(junk, OLD);
db.prepare('INSERT INTO fallback_config (model_db_id, priority, enabled) VALUES (?, 9999, 0)').run(junk);

purgeLegacyBazaarlinkDiscoveries(db);

expect(db.prepare('SELECT id FROM models WHERE id = ?').get(junk)).toBeUndefined();
// fallback_config is NO ACTION, so it must be cleared explicitly or the
// delete would fail the foreign key.
expect(db.prepare('SELECT model_db_id FROM fallback_config WHERE model_db_id = ?').get(junk)).toBeUndefined();
expect(db.prepare('SELECT model_db_id FROM model_availability WHERE model_db_id = ?').get(junk)).toBeUndefined();
});

it('keeps enabled models, seeded models, and later discoveries', () => {
const db = getDb();
const enabledDiscovered = addModel('bazaarlink', 'purge-enabled', 1);
markDiscovered(enabledDiscovered, OLD);

const seeded = addModel('bazaarlink', 'purge-seeded', 0); // no availability row

const laterDiscovery = addModel('bazaarlink', 'purge-later', 0);
markDiscovered(laterDiscovery, NEW);

const otherPlatform = addModel('huggingface', 'purge-other', 0);
markDiscovered(otherPlatform, OLD);

purgeLegacyBazaarlinkDiscoveries(db);

for (const [id, label] of [
[enabledDiscovered, 'enabled model'],
[seeded, 'seeded model'],
[laterDiscovery, 'later discovery'],
[otherPlatform, 'another provider'],
] as const) {
expect(db.prepare('SELECT id FROM models WHERE id = ?').get(id), `${label} must survive`).toBeDefined();
}
});

it('is idempotent', () => {
const db = getDb();
const junk = addModel('bazaarlink', 'purge-twice', 0);
markDiscovered(junk, OLD);

purgeLegacyBazaarlinkDiscoveries(db);
expect(() => purgeLegacyBazaarlinkDiscoveries(db)).not.toThrow();
expect(db.prepare('SELECT id FROM models WHERE id = ?').get(junk)).toBeUndefined();
});
});
53 changes: 53 additions & 0 deletions server/src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export async function initDb(dbPath?: string): Promise<Database.Database> {
seedModelCapabilities(db);
// Must follow seedModelCapabilities — that is where the image rows are seeded.
flagPaidGoogleModels(db);
purgeLegacyBazaarlinkDiscoveries(db);
ensureUnifiedKey(db);

// Auto-categorize all models on startup
Expand Down Expand Up @@ -1622,6 +1623,58 @@ function migrateModelsV17(db: Database.Database) {
apply();
}

/**
* One-time cleanup: remove the BazaarLink rows imported before the scout's
* chat filter existed.
*
* On 2026-07-20 the scout pulled BazaarLink's entire ~260-model catalog in
* batches of MAX_DISCOVERED_PER_PLATFORM, including ~245 text-to-image and
* text-to-video models (wan2.1-t2i, happyhorse, Z-Image-Turbo) and paid chat
* models that can never serve /v1/chat/completions. They landed disabled — so
* nothing could route to them — but they clutter the catalog and inflate the
* "unknown / never scanned" counters on Model Status.
*
* isBazaarlinkFreeChatEntry now rejects all of them at discovery (verified
* against the live catalog: 2 of 260 pass), so this only clears the historical
* residue. Scoped deliberately narrowly:
* - platform bazaarlink only (other providers import catalogs by design)
* - disabled only (never touch something in use)
* - auto-discovered only (seeded/curated rows have no discovery_source)
* - discovered before the cutoff, so a later legitimate discovery that an
* operator chooses to disable is never swept up
*
* fallback_config is ON DELETE NO ACTION and must be cleared first;
* model_availability / model_capabilities / model_runtime_health cascade.
*/
export function purgeLegacyBazaarlinkDiscoveries(db: Database.Database) {
const PRE_FILTER_CUTOFF = 'model-scout:2026-07-21';

const targets = (db.prepare(`
SELECT m.id
FROM models m
JOIN model_availability ma ON ma.model_db_id = m.id
WHERE m.platform = 'bazaarlink'
AND m.enabled = 0
AND ma.discovery_source IS NOT NULL
AND ma.discovery_source < ?
`).all(PRE_FILTER_CUTOFF) as { id: number }[]).map(r => r.id);

if (targets.length === 0) return;

const deleteFallback = db.prepare('DELETE FROM fallback_config WHERE model_db_id = ?');
const deleteModel = db.prepare('DELETE FROM models WHERE id = ?');

const apply = db.transaction(() => {
for (const id of targets) {
deleteFallback.run(id);
deleteModel.run(id);
}
});
apply();

console.log(`[Cleanup] Removed ${targets.length} legacy BazaarLink rows imported before the chat filter existed.`);
}

/**
* V18 (July 2026): retire the Google Gemma rows.
*
Expand Down
Loading