This commit is contained in:
Zacharias-Brohn
2026-01-15 14:46:19 +01:00
parent 6835af777b
commit 42e9516517
3 changed files with 8240 additions and 7377 deletions
+36 -7
View File
@@ -38,10 +38,15 @@ import { useDebouncedValue } from '@mantine/hooks';
import { deleteModel, getInstalledModels, pullModel, type OllamaModel } from '@/app/actions/ollama'; import { deleteModel, getInstalledModels, pullModel, type OllamaModel } from '@/app/actions/ollama';
// Type for the scraped models JSON // Type for the scraped models JSON
interface ModelInfo {
tags: string[];
capabilities: string[];
}
interface OllamaModelsData { interface OllamaModelsData {
generatedAt: string; generatedAt: string;
modelCount: number; modelCount: number;
models: Record<string, string[]>; models: Record<string, ModelInfo>;
} }
interface User { interface User {
@@ -540,17 +545,41 @@ export function SettingsModal({
}} }}
> >
{paginatedModels.map((modelName) => { {paginatedModels.map((modelName) => {
const tags = availableModels?.models[modelName] || []; const modelInfo = availableModels?.models[modelName];
const tags = modelInfo?.tags || [];
const capabilities = modelInfo?.capabilities || [];
const installedTags = getInstalledTags(modelName); const installedTags = getInstalledTags(modelName);
return ( return (
<Accordion.Item key={modelName} value={modelName}> <Accordion.Item key={modelName} value={modelName}>
<Accordion.Control> <Accordion.Control>
<Group justify="space-between" pr="sm"> <Group justify="space-between" pr="sm" wrap="nowrap">
<Text fw={500} size="sm"> <Group gap="xs" wrap="nowrap">
{modelName} <Text fw={500} size="sm">
</Text> {modelName}
<Group gap="xs"> </Text>
{capabilities.map((cap) => (
<Badge
key={cap}
size="xs"
variant="light"
color={
cap === 'vision'
? 'violet'
: cap === 'tools'
? 'blue'
: cap === 'thinking'
? 'orange'
: cap === 'embedding'
? 'teal'
: 'gray'
}
>
{cap}
</Badge>
))}
</Group>
<Group gap="xs" wrap="nowrap">
<Badge size="xs" variant="light" color="gray"> <Badge size="xs" variant="light" color="gray">
{tags.length} tags {tags.length} tags
</Badge> </Badge>
+8162 -7347
View File
File diff suppressed because it is too large Load Diff
+42 -23
View File
@@ -14,29 +14,45 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
const OLLAMA_LIBRARY_URL = 'https://ollama.com/library'; const OLLAMA_LIBRARY_URL = 'https://ollama.com/library';
/** /**
* Fetches the list of all available model names from Ollama's library page * Fetches the list of all available models with their capabilities from Ollama's library page
* @returns {Promise<Array<{name: string, capabilities: string[]}>>}
*/ */
async function fetchModelNames() { async function fetchModelsWithCapabilities() {
console.log('Fetching model list from Ollama library...'); console.log('Fetching model list from Ollama library...');
const response = await fetch(OLLAMA_LIBRARY_URL); const response = await fetch(OLLAMA_LIBRARY_URL);
const html = await response.text(); const html = await response.text();
// Extract model names using regex (matches href="/library/modelname") // Parse models and their capabilities from the HTML
const modelRegex = /href="\/library\/([^"\/]+)"/g; // Each model is in a <li x-test-model> block
const models = new Set(); const modelBlocks = html.split('<li x-test-model');
let match; const models = [];
while ((match = modelRegex.exec(html)) !== null) { for (let i = 1; i < modelBlocks.length; i++) {
// Filter out non-model links (like "tags" subpages) const block = modelBlocks[i];
const name = match[1];
if (name && !name.includes('/') && !name.includes(':')) { // Extract model name from href="/library/modelname"
models.add(name); const nameMatch = block.match(/href="\/library\/([^"\/]+)"/);
if (!nameMatch) continue;
const name = nameMatch[1];
if (name.includes('/') || name.includes(':')) continue;
// Extract capabilities from x-test-capability spans
const capabilities = [];
const capabilityRegex = /x-test-capability[^>]*>([^<]+)</g;
let capMatch;
while ((capMatch = capabilityRegex.exec(block)) !== null) {
const cap = capMatch[1].trim().toLowerCase();
if (cap && !capabilities.includes(cap)) {
capabilities.push(cap);
}
} }
models.push({ name, capabilities });
} }
const modelList = Array.from(models); console.log(`Found ${models.length} models`);
console.log(`Found ${modelList.length} models`); return models;
return modelList;
} }
/** /**
@@ -70,25 +86,28 @@ async function fetchModelTags(modelName) {
async function main() { async function main() {
const startTime = Date.now(); const startTime = Date.now();
// Fetch all model names // Fetch all models with their capabilities
const modelNames = await fetchModelNames(); const modelList = await fetchModelsWithCapabilities();
// Fetch tags for each model (with concurrency limit to be nice to the server) // Fetch tags for each model (with concurrency limit to be nice to the server)
const CONCURRENCY = 5; const CONCURRENCY = 5;
const models = {}; const models = {};
for (let i = 0; i < modelNames.length; i += CONCURRENCY) { for (let i = 0; i < modelList.length; i += CONCURRENCY) {
const batch = modelNames.slice(i, i + CONCURRENCY); const batch = modelList.slice(i, i + CONCURRENCY);
const results = await Promise.all( const results = await Promise.all(
batch.map(async (name) => { batch.map(async ({ name, capabilities }) => {
const tags = await fetchModelTags(name); const tags = await fetchModelTags(name);
return { name, tags }; return { name, tags, capabilities };
}) })
); );
for (const { name, tags } of results) { for (const { name, tags, capabilities } of results) {
models[name] = tags; models[name] = {
console.log(` ${name}: ${tags.length} tags`); tags,
capabilities,
};
console.log(` ${name}: ${tags.length} tags, capabilities: [${capabilities.join(', ')}]`);
} }
} }