diff --git a/crates/goose/src/providers/ollama.rs b/crates/goose/src/providers/ollama.rs index 1479a847fe73..442dfe0aa6fe 100644 --- a/crates/goose/src/providers/ollama.rs +++ b/crates/goose/src/providers/ollama.rs @@ -228,6 +228,51 @@ impl Provider for OllamaProvider { fn supports_streaming(&self) -> bool { self.supports_streaming } + + async fn fetch_supported_models(&self) -> Result>, ProviderError> { + // Ollama uses /api/tags endpoint to list installed models + let response = match self.api_client.response_get("api/tags").await { + Ok(resp) => resp, + Err(e) => { + tracing::warn!("Failed to fetch models from Ollama: {}", e); + return Ok(None); + } + }; + + // Parse JSON response + let json: serde_json::Value = match response.json().await { + Ok(json) => json, + Err(e) => { + tracing::warn!("Failed to parse Ollama models response: {}", e); + return Ok(None); + } + }; + + // Extract model names from the response + // Ollama returns: { "models": [{"name": "model1", "size": ..., "digest": ..., "modified_at": ...}, ...] } + let models = json + .get("models") + .and_then(|v| v.as_array()) + .map(|arr| { + let mut model_names: Vec = arr.iter() + .filter_map(|m| m.get("name").and_then(|v| v.as_str())) + .map(|s| s.to_string()) + .collect(); + model_names.sort(); + model_names + }); + + match models { + Some(model_list) if !model_list.is_empty() => { + tracing::info!("Found {} models in Ollama", model_list.len()); + Ok(Some(model_list)) + } + _ => { + tracing::info!("No models found in Ollama or unable to parse response"); + Ok(None) + } + } + } } impl OllamaProvider { diff --git a/ui/desktop/src/components/settings/models/modelInterface.ts b/ui/desktop/src/components/settings/models/modelInterface.ts index 4a397647dde8..c8253384d3c9 100644 --- a/ui/desktop/src/components/settings/models/modelInterface.ts +++ b/ui/desktop/src/components/settings/models/modelInterface.ts @@ -39,3 +39,25 @@ export async function getProviderMetadata( } return matches.metadata; } + +export async function getModelOptionsForProvider( + provider: ProviderDetails, + getProviderModels: (providerName: string) => Promise +): Promise<{ value: string; provider: string }[]> { + let models: string[] = []; + + try { + models = await getProviderModels(provider.name); + } catch (error) { + console.warn(`Failed to fetch models for ${provider.name}:`, error); + } + + if ((!models || models.length === 0) && provider.metadata.known_models?.length) { + models = provider.metadata.known_models.map((m) => m.name); + } + + return models.map((modelName) => ({ + value: modelName, + provider: provider.name, + })); +} diff --git a/ui/desktop/src/components/settings/models/subcomponents/AddModelModal.tsx b/ui/desktop/src/components/settings/models/subcomponents/AddModelModal.tsx index 88ef4e695249..83542b8d8bc8 100644 --- a/ui/desktop/src/components/settings/models/subcomponents/AddModelModal.tsx +++ b/ui/desktop/src/components/settings/models/subcomponents/AddModelModal.tsx @@ -16,7 +16,7 @@ import { Select } from '../../../ui/Select'; import { useConfig } from '../../../ConfigContext'; import { useModelAndProvider } from '../../../ModelAndProviderContext'; import type { View } from '../../../../utils/navigationUtils'; -import Model, { getProviderMetadata } from '../modelInterface'; +import Model, { getProviderMetadata, getModelOptionsForProvider } from '../modelInterface'; import { getPredefinedModelsFromEnv, shouldShowPredefinedModels } from '../predefinedModelsUtils'; type AddModelModalProps = { @@ -145,54 +145,27 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => { // Fetching models for all providers const modelPromises = activeProviders.map(async (p) => { - const providerName = p.name; - try { - let models = await getProviderModels(providerName); - // Fallback to known_models if server returned none - if ((!models || models.length === 0) && p.metadata.known_models?.length) { - models = p.metadata.known_models.map((m) => m.name); - } - return { provider: p, models, error: null }; - } catch (e: unknown) { - return { - provider: p, - models: null, - error: `Failed to fetch models for ${providerName}${e instanceof Error ? `: ${e.message}` : ''}`, - }; - } + const modelOptions = await getModelOptionsForProvider(p, getProviderModels); + return { provider: p, modelOptions }; }); const results = await Promise.all(modelPromises); - // Process results and build grouped options + // Build grouped options const groupedOptions: { options: { value: string; label: string; provider: string }[] }[] = []; - const errors: string[] = []; - - results.forEach(({ provider: p, models, error }) => { - if (error) { - errors.push(error); - // Fallback to metadata known_models on error - if (p.metadata.known_models && p.metadata.known_models.length > 0) { - groupedOptions.push({ - options: p.metadata.known_models.map(({ name }) => ({ - value: name, - label: name, - provider: p.name, - })), - }); - } - } else if (models && models.length > 0) { + + results.forEach(({ modelOptions }) => { + if (modelOptions.length > 0) { groupedOptions.push({ - options: models.map((m) => ({ value: m, label: m, provider: p.name })), + options: modelOptions.map((option) => ({ + value: option.value, + label: option.value, + provider: option.provider, + })), }); } }); - // Log errors if any providers failed (don't show to user) - if (errors.length > 0) { - console.error('Provider model fetch errors:', errors); - } - // Add the "Custom model" option to each provider group groupedOptions.forEach((group) => { const providerName = group.options[0]?.provider; diff --git a/ui/desktop/src/components/settings/models/subcomponents/LeadWorkerSettings.tsx b/ui/desktop/src/components/settings/models/subcomponents/LeadWorkerSettings.tsx index d2e255a76291..9655213105fc 100644 --- a/ui/desktop/src/components/settings/models/subcomponents/LeadWorkerSettings.tsx +++ b/ui/desktop/src/components/settings/models/subcomponents/LeadWorkerSettings.tsx @@ -6,6 +6,7 @@ import { Select } from '../../../ui/Select'; import { Input } from '../../../ui/input'; import { getPredefinedModelsFromEnv, shouldShowPredefinedModels } from '../predefinedModelsUtils'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../../../ui/dialog'; +import { getModelOptionsForProvider } from '../modelInterface'; interface LeadWorkerSettingsProps { isOpen: boolean; @@ -13,7 +14,7 @@ interface LeadWorkerSettingsProps { } export function LeadWorkerSettings({ isOpen, onClose }: LeadWorkerSettingsProps) { - const { read, upsert, getProviders, remove } = useConfig(); + const { read, upsert, getProviders, getProviderModels, remove } = useConfig(); const { currentModel } = useModelAndProvider(); const [leadModel, setLeadModel] = useState(''); const [workerModel, setWorkerModel] = useState(''); @@ -96,21 +97,21 @@ export function LeadWorkerSettings({ isOpen, onClose }: LeadWorkerSettingsProps) }); }); } else { - // Fallback to provider-based models + // Fetch models with dynamic discovery and static fallback const providers = await getProviders(false); const activeProviders = providers.filter((p) => p.is_configured); - activeProviders.forEach(({ metadata, name }) => { - if (metadata.known_models) { - metadata.known_models.forEach((model) => { - options.push({ - value: model.name, - label: `${model.name} (${metadata.display_name})`, - provider: name, - }); + // Fetch models for all active providers with dynamic discovery + for (const provider of activeProviders) { + const providerOptions = await getModelOptionsForProvider(provider, getProviderModels); + providerOptions.forEach((option) => { + options.push({ + value: option.value, + label: `${option.value} (${provider.metadata.display_name})`, + provider: option.provider, }); - } - }); + }); + } } setModelOptions(options); @@ -122,7 +123,7 @@ export function LeadWorkerSettings({ isOpen, onClose }: LeadWorkerSettingsProps) }; loadConfig(); - }, [read, getProviders, currentModel, isOpen]); + }, [read, getProviders, getProviderModels, currentModel, isOpen]); const handleSave = async () => { try {