diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 196eb25..b08555a 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -18,7 +18,7 @@ import ( ) // Version is set at build time via -ldflags, or defaults to dev -var Version = "0.4.5" +var Version = "0.4.6" func getEnvOrDefault(key, defaultValue string) string { if value := os.Getenv(key); value != "" { diff --git a/backend/internal/api/model_registry.go b/backend/internal/api/model_registry.go index 9564a87..a0e526a 100644 --- a/backend/internal/api/model_registry.go +++ b/backend/internal/api/model_registry.go @@ -491,6 +491,55 @@ func (s *ModelRegistryService) SyncModels(ctx context.Context, fetchDetails bool count++ } + // If fetchDetails is true and we have an Ollama client, update capabilities + // for installed models using the actual /api/show response (more accurate than scraped data) + if fetchDetails && s.ollamaClient != nil { + installedModels, err := s.ollamaClient.List(ctx) + if err != nil { + log.Printf("Warning: failed to list installed models for capability sync: %v", err) + } else { + log.Printf("Syncing capabilities for %d installed models", len(installedModels.Models)) + + for _, installed := range installedModels.Models { + select { + case <-ctx.Done(): + return count, ctx.Err() + default: + } + + // Extract base model name (e.g., "deepseek-r1" from "deepseek-r1:14b") + modelName := installed.Model + baseName := strings.Split(modelName, ":")[0] + + // Fetch real capabilities from Ollama + details, err := s.fetchModelDetails(ctx, modelName) + if err != nil { + log.Printf("Warning: failed to fetch details for %s: %v", modelName, err) + continue + } + + // Extract capabilities from the actual Ollama response + capabilities := []string{} + if details.Capabilities != nil { + for _, cap := range details.Capabilities { + capabilities = append(capabilities, string(cap)) + } + } + capsJSON, _ := json.Marshal(capabilities) + + // Update capabilities for the base model name + _, err = s.db.ExecContext(ctx, ` + UPDATE remote_models SET capabilities = ? WHERE slug = ? + `, string(capsJSON), baseName) + if err != nil { + log.Printf("Warning: failed to update capabilities for %s: %v", baseName, err) + } else { + log.Printf("Updated capabilities for %s: %v", baseName, capabilities) + } + } + } + } + return count, nil } diff --git a/frontend/package.json b/frontend/package.json index 6884ce6..4fcb122 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "vessel", - "version": "0.4.5", + "version": "0.4.6", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/lib/api/model-registry.ts b/frontend/src/lib/api/model-registry.ts index fbab6db..2a99b68 100644 --- a/frontend/src/lib/api/model-registry.ts +++ b/frontend/src/lib/api/model-registry.ts @@ -165,9 +165,11 @@ export async function fetchTagSizes(slug: string): Promise { /** * Sync models from ollama.com + * @param fetchDetails - If true, also fetches real capabilities from Ollama for installed models */ -export async function syncModels(): Promise { - const response = await fetch(`${API_BASE}/remote/sync`, { +export async function syncModels(fetchDetails: boolean = true): Promise { + const url = fetchDetails ? `${API_BASE}/remote/sync?details=true` : `${API_BASE}/remote/sync`; + const response = await fetch(url, { method: 'POST' }); diff --git a/frontend/src/routes/models/+page.svelte b/frontend/src/routes/models/+page.svelte index 7980047..0b72de0 100644 --- a/frontend/src/routes/models/+page.svelte +++ b/frontend/src/routes/models/+page.svelte @@ -40,12 +40,14 @@ let pullProgress = $state<{ status: string; completed?: number; total?: number } | null>(null); let pullError = $state(null); let loadingSizes = $state(false); + let capabilitiesVerified = $state(false); // True if capabilities come from Ollama (installed model) async function handleSelectModel(model: RemoteModel): Promise { selectedModel = model; selectedTag = model.tags[0] || ''; pullProgress = null; pullError = null; + capabilitiesVerified = false; // Fetch tag sizes if not already loaded if (!model.tagSizes || Object.keys(model.tagSizes).length === 0) { @@ -60,6 +62,21 @@ loadingSizes = false; } } + + // Try to fetch real capabilities from Ollama if model is installed locally + // This overrides scraped capabilities from ollama.com with accurate runtime data + try { + const realCapabilities = await modelsState.fetchCapabilities(model.slug); + // fetchCapabilities returns empty array on error, but we check hasCapability to confirm model exists + if (modelsState.hasCapability(model.slug, 'completion') || realCapabilities.length > 0) { + // Model is installed - use real capabilities from Ollama + selectedModel = { ...selectedModel!, capabilities: realCapabilities }; + capabilitiesVerified = true; + } + } catch { + // Model not installed locally - keep scraped capabilities + capabilitiesVerified = false; + } } function closeDetails(): void { @@ -693,6 +710,14 @@ ☁️ Cloud + + + + + + + from ollama.com + @@ -886,14 +911,40 @@ {/if} - {#if selectedModel.capabilities.length > 0} + {#if selectedModel.capabilities.length > 0 || !capabilitiesVerified}
-

Capabilities

-
- {#each selectedModel.capabilities as cap} - {cap} - {/each} -
+

+ Capabilities + {#if capabilitiesVerified} + + + + + verified + + {:else} + + + + + unverified + + {/if} +

+ {#if selectedModel.capabilities.length > 0} +
+ {#each selectedModel.capabilities as cap} + {cap} + {/each} +
+ {:else} +

No capabilities reported

+ {/if} + {#if !capabilitiesVerified} +

+ Install model to verify actual capabilities +

+ {/if}
{/if}