Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e19b6330e9 | |||
| c194a4e0e9 | |||
| 04c3018360 | |||
| 2699f1cd5c | |||
| 9f313e6599 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -41,3 +41,4 @@ CLAUDE.md
|
||||
dev.env
|
||||
backend/vessel-backend
|
||||
data/
|
||||
backend/data-dev/
|
||||
|
||||
@@ -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.8"
|
||||
|
||||
func getEnvOrDefault(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vessel",
|
||||
"version": "0.4.5",
|
||||
"version": "0.4.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -165,9 +165,11 @@ export async function fetchTagSizes(slug: string): Promise<RemoteModel> {
|
||||
|
||||
/**
|
||||
* Sync models from ollama.com
|
||||
* @param fetchDetails - If true, also fetches real capabilities from Ollama for installed models
|
||||
*/
|
||||
export async function syncModels(): Promise<SyncResponse> {
|
||||
const response = await fetch(`${API_BASE}/remote/sync`, {
|
||||
export async function syncModels(fetchDetails: boolean = true): Promise<SyncResponse> {
|
||||
const url = fetchDetails ? `${API_BASE}/remote/sync?details=true` : `${API_BASE}/remote/sync`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
|
||||
@@ -146,7 +146,8 @@ class LocalModelsState {
|
||||
const response = await checkForUpdates();
|
||||
|
||||
this.updatesAvailable = response.updatesAvailable;
|
||||
this.modelsWithUpdates = new Set(response.updates.map(m => m.name));
|
||||
// Handle null/undefined updates array from API
|
||||
this.modelsWithUpdates = new Set((response.updates ?? []).map(m => m.name));
|
||||
|
||||
return response;
|
||||
} catch (err) {
|
||||
|
||||
@@ -110,8 +110,25 @@ class ToolsState {
|
||||
return [];
|
||||
}
|
||||
|
||||
const definitions = toolRegistry.getDefinitions();
|
||||
return definitions.filter(def => this.isToolEnabled(def.function.name));
|
||||
// Get enabled builtin tools
|
||||
const builtinDefs = toolRegistry.getDefinitions();
|
||||
const enabled = builtinDefs.filter(def => this.isToolEnabled(def.function.name));
|
||||
|
||||
// Add enabled custom tools
|
||||
for (const custom of this.customTools) {
|
||||
if (custom.enabled && this.isToolEnabled(custom.name)) {
|
||||
enabled.push({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: custom.name,
|
||||
description: custom.description,
|
||||
parameters: custom.parameters
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,12 +40,14 @@
|
||||
let pullProgress = $state<{ status: string; completed?: number; total?: number } | null>(null);
|
||||
let pullError = $state<string | null>(null);
|
||||
let loadingSizes = $state(false);
|
||||
let capabilitiesVerified = $state(false); // True if capabilities come from Ollama (installed model)
|
||||
|
||||
async function handleSelectModel(model: RemoteModel): Promise<void> {
|
||||
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 {
|
||||
@@ -219,7 +236,10 @@
|
||||
// Initialize stores (backend handles heavy operations)
|
||||
localModelsState.init();
|
||||
modelRegistry.init();
|
||||
modelsState.refresh();
|
||||
modelsState.refresh().then(() => {
|
||||
// Fetch capabilities for all installed models
|
||||
modelsState.fetchAllCapabilities();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -476,6 +496,7 @@
|
||||
{:else}
|
||||
<div class="space-y-2">
|
||||
{#each localModelsState.models as model (model.name)}
|
||||
{@const caps = modelsState.getCapabilities(model.name) ?? []}
|
||||
<div class="group rounded-lg border border-theme bg-theme-secondary p-4 transition-colors hover:border-theme-subtle">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
@@ -496,6 +517,36 @@
|
||||
<span>Parameters: {model.parameterSize}</span>
|
||||
<span>Quantization: {model.quantizationLevel}</span>
|
||||
</div>
|
||||
<!-- Capabilities (from Ollama runtime - verified) -->
|
||||
{#if caps.length > 0}
|
||||
<div class="mt-2 flex flex-wrap gap-1.5">
|
||||
{#if caps.includes('vision')}
|
||||
<span class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs bg-purple-900/50 text-purple-300">
|
||||
<span>👁</span><span>Vision</span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if caps.includes('tools')}
|
||||
<span class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs bg-blue-900/50 text-blue-300">
|
||||
<span>🔧</span><span>Tools</span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if caps.includes('thinking')}
|
||||
<span class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs bg-pink-900/50 text-pink-300">
|
||||
<span>🧠</span><span>Thinking</span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if caps.includes('embedding')}
|
||||
<span class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs bg-amber-900/50 text-amber-300">
|
||||
<span>📊</span><span>Embedding</span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if caps.includes('code')}
|
||||
<span class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs bg-emerald-900/50 text-emerald-300">
|
||||
<span>💻</span><span>Code</span>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if deleteConfirm === model.name}
|
||||
@@ -693,6 +744,14 @@
|
||||
<span>☁️</span>
|
||||
<span>Cloud</span>
|
||||
</button>
|
||||
|
||||
<!-- Capability info notice -->
|
||||
<span class="ml-2 text-xs text-theme-muted" title="Capability data is sourced from ollama.com and may not be accurate. Actual capabilities are verified once a model is installed locally.">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="inline h-3.5 w-3.5 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="opacity-60">from ollama.com</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Size Range Filters -->
|
||||
@@ -886,14 +945,40 @@
|
||||
{/if}
|
||||
|
||||
<!-- Capabilities -->
|
||||
{#if selectedModel.capabilities.length > 0}
|
||||
{#if selectedModel.capabilities.length > 0 || !capabilitiesVerified}
|
||||
<div class="mb-6">
|
||||
<h3 class="mb-2 text-sm font-medium text-theme-secondary">Capabilities</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each selectedModel.capabilities as cap}
|
||||
<span class="rounded bg-theme-tertiary px-2 py-1 text-xs text-theme-secondary">{cap}</span>
|
||||
{/each}
|
||||
</div>
|
||||
<h3 class="mb-2 flex items-center gap-2 text-sm font-medium text-theme-secondary">
|
||||
<span>Capabilities</span>
|
||||
{#if capabilitiesVerified}
|
||||
<span class="inline-flex items-center gap-1 rounded bg-green-900/30 px-1.5 py-0.5 text-xs text-green-400" title="Capabilities verified from installed model">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
verified
|
||||
</span>
|
||||
{:else}
|
||||
<span class="inline-flex items-center gap-1 rounded bg-amber-900/30 px-1.5 py-0.5 text-xs text-amber-400" title="Capabilities sourced from ollama.com - install model for verified data">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
unverified
|
||||
</span>
|
||||
{/if}
|
||||
</h3>
|
||||
{#if selectedModel.capabilities.length > 0}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each selectedModel.capabilities as cap}
|
||||
<span class="rounded bg-theme-tertiary px-2 py-1 text-xs text-theme-secondary">{cap}</span>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-xs text-theme-muted">No capabilities reported</p>
|
||||
{/if}
|
||||
{#if !capabilitiesVerified}
|
||||
<p class="mt-2 text-xs text-theme-muted">
|
||||
Install model to verify actual capabilities
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user