diff --git a/src/cli/commands/setup.ts b/src/cli/commands/setup.ts index 2bca27c5f..eb72ac7b2 100644 --- a/src/cli/commands/setup.ts +++ b/src/cli/commands/setup.ts @@ -261,7 +261,7 @@ async function handlePluginSetup( selectedModel = preselectedModel; logger.success(`Model selected automatically: ${selectedModel}`); } else { - selectedModel = await promptForModelSelection(models, providerTemplate); + selectedModel = await promptForModelSelection(models, providerTemplate, setupSteps, credentials); } // Step 3.5: Install model if provider supports it (e.g., Ollama) @@ -446,14 +446,40 @@ async function promptForProfileName(providerName: string): Promise { */ async function promptForModelSelection( models: string[], - providerTemplate?: any + providerTemplate?: any, + setupSteps?: any, + credentials?: any ): Promise { + const canSearch = typeof setupSteps?.searchModel === 'function'; + if (models.length === 0) { + if (canSearch) { + const { entryMethod } = await inquirer.prompt([ + { + type: 'list', + name: 'entryMethod', + message: 'No models found. How would you like to choose a model?', + choices: [ + { name: chalk.cyan('🔍 Search Ollama library...'), value: 'search' }, + { name: 'Enter model name manually', value: 'manual' } + ] + } + ]); + + if (entryMethod === 'search') { + const searched = await setupSteps.searchModel(credentials); + if (searched) { + return searched; + } + // User backed out of search - fall through to manual entry + } + } + const { manualModel } = await inquirer.prompt([ { type: 'input', name: 'manualModel', - message: 'No models found. Enter model name manually:', + message: 'Enter model name manually:', default: providerTemplate?.recommendedModels?.[0] || 'gpt-5.5', validate: (input: string) => input.trim() !== '' || 'Model name is required' } @@ -461,35 +487,63 @@ async function promptForModelSelection( return manualModel ? manualModel.trim() : manualModel; } - // Use getAllModelChoices for enriched display with metadata - const choices = [ - ...getAllModelChoices(models, providerTemplate), - { name: chalk.white('Custom model (manual entry)...'), value: 'custom' } - ]; - - const { selectedModel } = await inquirer.prompt([ - { - type: 'list', - name: 'selectedModel', - message: `Choose a model (${models.length} available):`, - choices, - pageSize: 15 + // Live-computed recommendations (fits this machine + agent-ready + most + // popular) when the provider supports it; falls back to the template's + // static recommendedModels inside getAllModelChoices otherwise. + let recommendedOverrideIds: Set | undefined; + if (typeof setupSteps?.getRecommendedModels === 'function') { + const recommendSpinner = ora('Finding recommended models...').start(); + try { + const recommended = await setupSteps.getRecommendedModels(models, credentials); + recommendedOverrideIds = new Set(recommended); + recommendSpinner.stop(); + } catch { + recommendSpinner.stop(); + // Non-fatal - just show the list without recommendations. } - ]); + } + + // Loop so backing out of search re-shows this list instead of dead-ending + for (;;) { + // Use getAllModelChoices for enriched display with metadata + const choices = [ + ...getAllModelChoices(models, providerTemplate, recommendedOverrideIds), + { name: chalk.white('Custom model (manual entry)...'), value: 'custom' }, + ...(canSearch ? [{ name: chalk.cyan('🔍 Search Ollama library...'), value: 'search' }] : []) + ]; - if (selectedModel === 'custom') { - const { customModel } = await inquirer.prompt([ + const { selectedModel } = await inquirer.prompt([ { - type: 'input', - name: 'customModel', - message: 'Enter model name:', - validate: (input: string) => input.trim() !== '' || 'Model is required' + type: 'list', + name: 'selectedModel', + message: `Choose a model (${models.length} available):`, + choices, + pageSize: 15 } ]); - return customModel ? customModel.trim() : customModel; - } - return selectedModel; + if (selectedModel === 'search') { + const searched = await setupSteps.searchModel(credentials); + if (searched) { + return searched; + } + continue; + } + + if (selectedModel === 'custom') { + const { customModel } = await inquirer.prompt([ + { + type: 'input', + name: 'customModel', + message: 'Enter model name:', + validate: (input: string) => input.trim() !== '' || 'Model is required' + } + ]); + return customModel ? customModel.trim() : customModel; + } + + return selectedModel; + } } /** diff --git a/src/providers/core/types.ts b/src/providers/core/types.ts index e34552bd1..8d901e207 100644 --- a/src/providers/core/types.ts +++ b/src/providers/core/types.ts @@ -315,6 +315,23 @@ export interface ProviderSetupSteps { template?: ProviderTemplate ): Promise; + /** + * Optional: interactive live search against an external model catalog + * (e.g. Ollama's model library). Runs its own prompts and returns the + * chosen model id, or null if the user cancelled/backed out - the caller + * falls back to the normal model list in that case. + */ + searchModel?(credentials: ProviderCredentials): Promise; + + /** + * Optional: compute which of the given models should be marked/starred + * as recommended, using live signals (fits the current machine, has the + * capabilities a coding agent needs, real-world popularity) instead of a + * static hardcoded list. Returning fewer/no ids is fine - callers treat + * this as "no recommendation" rather than an error. + */ + getRecommendedModels?(models: string[], credentials: ProviderCredentials): Promise; + /** * Step 3: Build final configuration * diff --git a/src/providers/integration/setup-ui.ts b/src/providers/integration/setup-ui.ts index 2e2ffd229..e778c4732 100644 --- a/src/providers/integration/setup-ui.ts +++ b/src/providers/integration/setup-ui.ts @@ -229,8 +229,10 @@ export function formatModelChoice( * Get all model choices with metadata * * Returns array of formatted model choices, sorted by: - * 1. Recommended models first — only the latest version within each - * recommendedModels family (see computeRecommendedModelIds) + * 1. Recommended models first — either `recommendedOverrideIds` (a live, + * provider-computed set - see ProviderSetupSteps.getRecommendedModels) + * when given, or only the latest version within each of the template's + * static recommendedModels families otherwise (see computeRecommendedModelIds) * 2. Alphabetically by model ID * * Choices whose declared memory requirement (modelMetadata.minMemoryGb) @@ -238,9 +240,10 @@ export function formatModelChoice( */ export function getAllModelChoices( models: string[], - template?: ProviderTemplate + template?: ProviderTemplate, + recommendedOverrideIds?: Set ): Array<{ name: string; value: string; disabled?: boolean | string }> { - const recommendedIds = computeRecommendedModelIds(models, template?.recommendedModels); + const recommendedIds = recommendedOverrideIds ?? computeRecommendedModelIds(models, template?.recommendedModels); // Sort models using common rules const sortedModels = [...models].sort((a, b) => { diff --git a/src/providers/plugins/ollama/ollama.library.ts b/src/providers/plugins/ollama/ollama.library.ts new file mode 100644 index 000000000..333efab9a --- /dev/null +++ b/src/providers/plugins/ollama/ollama.library.ts @@ -0,0 +1,139 @@ +/** + * Ollama Library Search + * + * Ollama has no public JSON API for its model library - only the + * server-rendered HTML pages at ollama.com/search and + * ollama.com/library//tags. This scrapes those pages so the setup + * wizard can offer live search instead of the narrow static/cloud-catalog + * list in ollama.models.ts. Unofficial by nature: any parse failure must be + * treated as non-fatal by callers (they fall back to manual entry). + */ + +import { HTTPClient } from '../../core/base/http-client.js'; + +const OLLAMA_WEB_BASE_URL = 'https://ollama.com'; +const MAX_SEARCH_RESULTS = 15; + +const client = new HTTPClient({ timeout: 10000 }); + +export interface OllamaLibraryModel { + name: string; + description?: string; +} + +function decodeHtmlEntities(text: string): string { + return text + .replace(/&/g, '&') + .replace(/'/g, "'") + .replace(/"/g, '"') + .replace(/</g, '<') + .replace(/>/g, '>') + .trim(); +} + +/** + * Search Ollama's model library (ollama.com/search). + * Returns base model names (no tag) in the page's own order (popular first). + */ +export async function searchOllamaLibrary(query: string): Promise { + const url = `${OLLAMA_WEB_BASE_URL}/search?q=${encodeURIComponent(query)}`; + const response = await client.getRaw(url); + + if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) { + throw new Error(`Ollama library search failed: HTTP ${response.statusCode}`); + } + + const results: OllamaLibraryModel[] = []; + const seen = new Set(); + + // Each result is an
  • card containing a `/library/` link followed + // by an

    title and a

    description. Base model links never contain + // ":" (tag links do), so filtering on that distinguishes result cards from + // any tag references elsewhere on the page. + const cardPattern = /href="\/library\/([a-zA-Z0-9._-]+)"[\s\S]{0,600}?<\/h2>\s*]*>([\s\S]*?)<\/p>/g; + let match: RegExpExecArray | null; + + while ((match = cardPattern.exec(response.data)) && results.length < MAX_SEARCH_RESULTS) { + const [, slug, rawDescription] = match; + if (slug.includes(':') || seen.has(slug)) { + continue; + } + seen.add(slug); + + const description = decodeHtmlEntities(rawDescription.replace(/<[^>]+>/g, '')); + results.push({ name: slug, description: description || undefined }); + } + + if (results.length === 0) { + throw new Error(`No parsable results for "${query}" - ollama.com's page structure may have changed`); + } + + return results; +} + +/** + * List the installable tags/variants for a model (ollama.com/library//tags). + * ":latest" (or the bare model id when no ":latest" tag exists) is returned first. + */ +export async function listOllamaModelTags(modelSlug: string): Promise { + const url = `${OLLAMA_WEB_BASE_URL}/library/${encodeURIComponent(modelSlug)}/tags`; + const response = await client.getRaw(url); + + if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) { + throw new Error(`Failed to fetch tags for "${modelSlug}": HTTP ${response.statusCode}`); + } + + const escapedSlug = modelSlug.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const tagPattern = new RegExp(`href="/library/(${escapedSlug}:[a-zA-Z0-9._-]+)"`, 'g'); + const tags = new Set(); + let match: RegExpExecArray | null; + + while ((match = tagPattern.exec(response.data))) { + tags.add(match[1]); + } + + if (tags.size === 0) { + throw new Error(`No tags found for "${modelSlug}" - ollama.com's page structure may have changed`); + } + + const latestTag = `${modelSlug}:latest`; + const rest = [...tags].filter(tag => tag !== latestTag).sort(); + return tags.has(latestTag) ? [latestTag, ...rest] : rest; +} + +export interface OllamaModelDetails { + /** Approximate pull/download count parsed from the model page (0 if unknown). */ + downloads: number; + /** Whether the model's page advertises tool/function-calling support. */ + supportsTools: boolean; +} + +function parseCountSuffix(raw: string): number { + const match = raw.replace(/,/g, '').match(/^([\d.]+)\s*([KMB])?$/i); + if (!match) { + return 0; + } + const value = parseFloat(match[1]); + const multiplier = { K: 1e3, M: 1e6, B: 1e9 }[match[2]?.toUpperCase() as 'K' | 'M' | 'B'] ?? 1; + return Math.round(value * multiplier); +} + +/** + * Fetch a model's download count and tool-support badge from its + * ollama.com/library/ page - used to rank live recommendations + * (popularity + agentic capability) instead of a hardcoded list. + */ +export async function getOllamaModelDetails(modelSlug: string): Promise { + const url = `${OLLAMA_WEB_BASE_URL}/library/${encodeURIComponent(modelSlug)}`; + const response = await client.getRaw(url); + + if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) { + throw new Error(`Failed to fetch model page for "${modelSlug}": HTTP ${response.statusCode}`); + } + + const downloadsMatch = response.data.match(/([\d.,]+[KMB]?)<\/span>\s*]*> Downloads<\/span>/i); + const downloads = downloadsMatch ? parseCountSuffix(downloadsMatch[1]) : 0; + const supportsTools = /class="[^"]*"\s*>\s*tools\s*<\/span>/i.test(response.data); + + return { downloads, supportsTools }; +} diff --git a/src/providers/plugins/ollama/ollama.models.ts b/src/providers/plugins/ollama/ollama.models.ts index 6401da044..a9f52f817 100644 --- a/src/providers/plugins/ollama/ollama.models.ts +++ b/src/providers/plugins/ollama/ollama.models.ts @@ -104,25 +104,11 @@ function isCodingModel(modelName: string): boolean { } /** - * Get metadata for a coding model from OllamaTemplate + * Get metadata for a coding model based on its name pattern */ function getCodingModelMetadata(modelId: string): Partial { - // Extract base name (without tag) const baseName = modelId.split(':')[0]; - // Get metadata from template (single source of truth) - - // prefer the full id, fall back to the base name - const metadata = OllamaTemplate.modelMetadata?.[modelId] ?? OllamaTemplate.modelMetadata?.[baseName]; - - if (metadata) { - return { - name: metadata.name, - description: metadata.description, - popular: metadata.popular ?? false - }; - } - - // If not in template but matches coding pattern, mark as coding model if (isCodingModel(baseName)) { return { name: modelId, @@ -193,7 +179,9 @@ export class OllamaModelProxy extends BaseModelProxy { /** * Fetch available models (for setup/discovery) * Returns installed models merged with the ollama.com cloud catalog - * (public endpoint); falls back to recommended models otherwise. + * (public endpoint). Live discovery beyond this narrow set happens via + * OllamaSetupSteps.searchModel (ollama.setup-steps.ts), which searches + * Ollama's full model library on demand instead of a hardcoded list. * An API key is only needed to *run* cloud models directly on * ollama.com, not to list them. */ @@ -239,20 +227,7 @@ export class OllamaModelProxy extends BaseModelProxy { logger.debug('Failed to fetch Ollama cloud models:', error); } - if (merged.size > 0) { - return [...merged.values()]; - } - - // Fall back to template's recommended models with metadata from template - return OllamaTemplate.recommendedModels.map(modelId => { - const metadata = OllamaTemplate.modelMetadata?.[modelId]; - return { - id: modelId, - name: metadata?.name || modelId, - description: metadata?.description, - popular: metadata?.popular ?? true // All recommended models are popular by default - }; - }); + return [...merged.values()]; } /** @@ -354,20 +329,6 @@ export class OllamaModelProxy extends BaseModelProxy { return null; } - // Get detailed info from template if available - const baseName = modelName.split(':')[0]; - const templateMetadata = OllamaTemplate.modelMetadata?.[modelName] ?? OllamaTemplate.modelMetadata?.[baseName]; - - if (templateMetadata) { - return { - ...basicInfo, - name: templateMetadata.name, - description: templateMetadata.description, - popular: templateMetadata.popular, - contextWindow: templateMetadata.contextWindow - }; - } - return basicInfo; } catch { return null; diff --git a/src/providers/plugins/ollama/ollama.setup-steps.ts b/src/providers/plugins/ollama/ollama.setup-steps.ts index 5784b48b3..609c126c0 100644 --- a/src/providers/plugins/ollama/ollama.setup-steps.ts +++ b/src/providers/plugins/ollama/ollama.setup-steps.ts @@ -16,6 +16,34 @@ import { ProviderRegistry } from '../../core/registry.js'; import { OllamaTemplate } from './ollama.template.js'; import { toCloudOffloadTag } from './ollama.models.js'; +/** + * Split a base model slug (no tag) into its family root and version, e.g. + * "qwen3.8" -> { family: "qwen", version: [3, 8] }, "glm-5.1" -> { family: + * "glm", version: [5, 1] }. Slugs with no trailing version number (e.g. + * "gpt-oss", or tier variants like "nemotron-3-super") get their own + * singleton family - only sequential releases of the same lineage collapse + * together, not parallel size/tier variants. + */ +function familyAndVersion(slug: string): { family: string; version: number[] } { + const match = slug.match(/^(.*?)-?(\d+(?:\.\d+)*)$/); + if (!match) { + return { family: slug, version: [] }; + } + return { family: match[1], version: match[2].split('.').map(Number) }; +} + +// Descending comparator: negative means `a` is the newer/higher version. +function compareVersionsDesc(a: number[], b: number[]): number { + const len = Math.max(a.length, b.length); + for (let i = 0; i < len; i++) { + const diff = (b[i] ?? 0) - (a[i] ?? 0); + if (diff !== 0) { + return diff; + } + } + return 0; +} + /** * Ollama setup steps implementation * @@ -147,9 +175,10 @@ export const OllamaSetupSteps: ProviderSetupSteps = { /** * Fetch available models from Ollama * - * Offers installed local models, the template's curated recommendations, - * and the ollama.com cloud catalog (capability filtering happens in the - * selection UI via modelMetadata). + * Offers installed local models and the ollama.com cloud catalog - both + * live data, no hardcoded list. Anything beyond that narrow set is found + * via searchModel() below, which searches Ollama's full model library on + * demand. * * For local setups (daemon at localhost) cloud catalog entries are mapped * to their local cloud-offload tag (`gpt-oss:120b` -> `gpt-oss:120b-cloud`, @@ -179,12 +208,203 @@ export const OllamaSetupSteps: ProviderSetupSteps = { : m.id ); - return [...new Set([...ids, ...OllamaTemplate.recommendedModels])]; + return [...new Set(ids)]; + } catch { + // If fetch fails, return empty so setup prompts the user to search or + // enter a model manually instead of showing a static, possibly stale list. + return []; + } + }, + + /** + * Mark up to 3 of the fetched models as recommended, using live signals + * instead of a hardcoded list: + * - Fits the current machine (cloud models always do - they run on + * ollama.com; local models are checked against their real installed + * size vs. this machine's usable RAM). + * - Supports tools/function-calling (most coding agents require it - + * see the "tools" capability badge on the model's ollama.com page). + * - Ranked by real-world popularity (download count on that same page) - + * but grouped by model lineage first (e.g. qwen3.5/3.6/3.8 all count + * toward one "qwen" score) so an older release's download lead doesn't + * bury its own newer version; the top 3 lineages are returned, each + * represented by its most recent version (qwen3.5 may be the most + * downloaded, but qwen3.8 - the newest - is what gets recommended). + */ + async getRecommendedModels(models: string[], credentials: ProviderCredentials): Promise { + if (models.length === 0) { + return []; + } + + const { OllamaModelProxy } = await import('./ollama.models.js'); + const { getOllamaModelDetails } = await import('./ollama.library.js'); + const { detectSystemCapabilities } = await import('../../../utils/hardware.js'); + + const modelProxy = new OllamaModelProxy(credentials.baseUrl, credentials.apiKey); + + // Real installed sizes, for the environment-fit check below. + const sizeById = new Map(); + try { + for (const model of await modelProxy.listModels()) { + if (model.size) { + sizeById.set(model.id, model.size); + } + } + } catch { + // No local daemon reachable - every remaining id is a cloud one anyway. + } + + let usableMemoryGb = Infinity; + try { + usableMemoryGb = (await detectSystemCapabilities()).usableMemoryGb; } catch { - // If fetch fails, return empty so setup prompts the user to enter a - // model manually instead of showing a static, possibly stale list. + // Can't probe hardware - don't let that block recommendations. + } + + const isCloudHost = (credentials.baseUrl || '').includes('ollama.com'); + const isCloudId = (id: string): boolean => isCloudHost || id.endsWith('-cloud') || id.endsWith(':cloud'); + + const fitsEnvironment = (id: string): boolean => { + if (isCloudId(id)) { + return true; + } + const bytes = sizeById.get(id); + if (!bytes) { + return true; // No size data - don't penalize for missing info + } + return bytes / 1024 ** 3 <= usableMemoryGb; + }; + + // Bound worst case (e.g. many locally-installed models) before the + // per-model network fetch below. + const candidates = models.filter(fitsEnvironment).slice(0, 25); + if (candidates.length === 0) { return []; } + + const scored = await Promise.all( + candidates.map(async id => { + const baseSlug = id.split(':')[0]; + try { + const details = await getOllamaModelDetails(baseSlug); + return { id, ...details }; + } catch { + return { id, downloads: 0, supportsTools: false }; + } + }) + ); + + // Group by lineage so an older release's accumulated downloads don't + // bury its own newer version; each group is scored by its most + // downloaded member but represented by its most recent one. + interface FamilyGroup { + popularityScore: number; + representativeId: string; + representativeVersion: number[]; + } + const families = new Map(); + + for (const s of scored.filter(s => s.supportsTools)) { + const { family, version } = familyAndVersion(s.id.split(':')[0]); + const existing = families.get(family); + + if (!existing) { + families.set(family, { popularityScore: s.downloads, representativeId: s.id, representativeVersion: version }); + continue; + } + + existing.popularityScore = Math.max(existing.popularityScore, s.downloads); + if (compareVersionsDesc(version, existing.representativeVersion) < 0) { + existing.representativeId = s.id; + existing.representativeVersion = version; + } + } + + return [...families.values()] + .sort((a, b) => b.popularityScore - a.popularityScore) + .slice(0, 3) + .map(g => g.representativeId); + }, + + /** + * Interactive live search against ollama.com's model library. + * + * The fetchModels() list only covers installed/cloud-catalog models; this + * lets a user find and install anything else in Ollama's public library + * (e.g. a model that isn't already installed and isn't in the curated + * cloud catalog) without knowing its exact id up front. + */ + async searchModel(_credentials: ProviderCredentials): Promise { + const inquirer = (await import('inquirer')).default; + const ora = (await import('ora')).default; + const chalk = (await import('chalk')).default; + const { searchOllamaLibrary, listOllamaModelTags } = await import('./ollama.library.js'); + + const { query } = await inquirer.prompt([ + { + type: 'input', + name: 'query', + message: 'Search Ollama library:' + } + ]); + + if (!query || !query.trim()) { + return null; + } + + const searchSpinner = ora(`Searching Ollama library for "${query.trim()}"...`).start(); + let results; + try { + results = await searchOllamaLibrary(query.trim()); + searchSpinner.succeed(chalk.green(`Found ${results.length} model(s)`)); + } catch (error) { + searchSpinner.fail(chalk.red('Search failed')); + console.log(chalk.dim(` ${error instanceof Error ? error.message : 'Unknown error'}\n`)); + return null; + } + + const { picked } = await inquirer.prompt([ + { + type: 'list', + name: 'picked', + message: 'Select a model:', + pageSize: 15, + choices: [ + ...results.map(r => ({ + name: r.description ? `${r.name} ${chalk.dim(`- ${r.description}`)}` : r.name, + value: r.name + })), + { name: chalk.dim('← Back'), value: null } + ] + } + ]); + + if (!picked) { + return null; + } + + const tagSpinner = ora(`Fetching available sizes for ${picked}...`).start(); + let tags: string[] = []; + try { + tags = await listOllamaModelTags(picked); + tagSpinner.succeed(chalk.green(`Found ${tags.length} variant(s)`)); + } catch (error) { + tagSpinner.warn(chalk.yellow('Could not fetch size variants - using default')); + console.log(chalk.dim(` ${error instanceof Error ? error.message : 'Unknown error'}\n`)); + return picked; + } + + const { tag } = await inquirer.prompt([ + { + type: 'list', + name: 'tag', + message: `Choose a variant of ${picked}:`, + pageSize: 15, + choices: tags + } + ]); + + return tag; }, /** diff --git a/src/providers/plugins/ollama/ollama.template.ts b/src/providers/plugins/ollama/ollama.template.ts index 547ee4167..0665b677f 100644 --- a/src/providers/plugins/ollama/ollama.template.ts +++ b/src/providers/plugins/ollama/ollama.template.ts @@ -18,31 +18,11 @@ export const OllamaTemplate = registerProvider({ defaultBaseUrl: 'http://localhost:11434', requiresAuth: false, authType: 'none', - recommendedModels: [ - 'qwen2.5-coder', - 'gpt-oss:120b-cloud', - 'deepseek-coder-v2' - ], - modelMetadata: { - 'qwen2.5-coder': { - name: 'Qwen 2.5 Coder', - description: 'Excellent coding model with tool support (7B, ~5GB download)', - popular: true, - minMemoryGb: 8 - }, - 'gpt-oss:120b-cloud': { - name: 'GPT-OSS 120B (cloud)', - description: 'OpenAI open-weight model on Ollama cloud - no local memory required', - popular: true - // No minMemoryGb: runs on ollama.com, not on the local machine - }, - 'deepseek-coder-v2': { - name: 'DeepSeek Coder V2', - description: 'Advanced coding model with tool support (16B, ~9GB download)', - popular: true, - minMemoryGb: 12 - } - }, + // No hardcoded model list: the setup wizard offers installed local models, + // the ollama.com cloud catalog, and live search against Ollama's full + // model library (OllamaSetupSteps.searchModel in ollama.setup-steps.ts) - + // all real data instead of a curated set that inevitably goes stale. + recommendedModels: [], capabilities: ['streaming', 'tools', 'embeddings', 'model-management'], supportsModelInstallation: true, healthCheckEndpoint: '/api/version', @@ -101,15 +81,15 @@ curl -fsSL https://ollama.com/install.sh | sh ### Windows Download from: https://ollama.com/download -## Recommended Coding Models (Tool Support Required) - -**Important**: Some agents require models with function calling/tool support. +## Choosing a Model (Tool Support Required) -- **qwen2.5-coder**: Excellent for coding tasks with tool support (7B, ~5GB) -- **gpt-oss:120b-cloud**: OpenAI's open-weight model via Ollama cloud (120B, no download) -- **deepseek-coder-v2**: Advanced coding model with tool support (16B, ~9GB) +**Important**: Some agents require models with function calling/tool support +(models without it, like plain codellama, will fail with those agents). -**Note**: Models without tool support (like codellama) will fail with agents that require function calling. +Setup shows installed local models plus the ollama.com cloud catalog, and +offers a "Search Ollama library..." option to find and install anything else +in Ollama's full model library on demand - no need to remember exact model +names or tags in advance. ## Ollama Cloud (ollama.com)