Skip to content
Closed
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
45 changes: 45 additions & 0 deletions crates/goose/src/providers/ollama.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,51 @@ impl Provider for OllamaProvider {
fn supports_streaming(&self) -> bool {
self.supports_streaming
}

async fn fetch_supported_models(&self) -> Result<Option<Vec<String>>, 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<String> = 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 {
Expand Down
22 changes: 22 additions & 0 deletions ui/desktop/src/components/settings/models/modelInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,25 @@ export async function getProviderMetadata(
}
return matches.metadata;
}

export async function getModelOptionsForProvider(
provider: ProviderDetails,
getProviderModels: (providerName: string) => Promise<string[]>
): 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,
}));
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ 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;
onClose: () => void;
}

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<string>('');
const [workerModel, setWorkerModel] = useState<string>('');
Expand Down Expand Up @@ -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);
Expand All @@ -122,7 +123,7 @@ export function LeadWorkerSettings({ isOpen, onClose }: LeadWorkerSettingsProps)
};

loadConfig();
}, [read, getProviders, currentModel, isOpen]);
}, [read, getProviders, getProviderModels, currentModel, isOpen]);

const handleSave = async () => {
try {
Expand Down
Loading