6 Commits

Author SHA1 Message Date
3c8d811cdc chore: bump version to 0.4.3
Some checks failed
Create Release / release (push) Has been cancelled
2026-01-02 21:05:03 +01:00
5cab71dd78 fix: sync context progress bar with custom context length setting
- Add customMaxTokens override to ContextManager
- maxTokens is now derived from custom override or model default
- ChatWindow syncs settings.num_ctx to context manager
- Progress bar now shows custom context length when enabled
2026-01-02 21:04:47 +01:00
41bee19f6b chore: bump version to 0.4.2
Some checks failed
Create Release / release (push) Has been cancelled
2026-01-02 20:54:55 +01:00
f4febf8973 fix: initialize custom parameters from model defaults
- Fetch actual model defaults from Ollama's /api/show endpoint
- Parse YAML-like parameters field (e.g., "temperature 0.7")
- Cache model defaults to avoid repeated API calls
- Initialize sliders with model's actual values when enabling custom params
- Show asterisk indicator when parameter differs from model default
- Reset button now restores to model defaults, not hardcoded values
2026-01-02 20:52:47 +01:00
a552f4a223 chore: bump version to 0.4.1
Some checks failed
Create Release / release (push) Has been cancelled
2026-01-02 20:36:03 +01:00
4a9e45b40b fix: persist toolCalls to database for reload persistence
Tool usage was not showing after page reload because the toolCalls
field was not being included when saving assistant messages to the
database. Now toolCalls are properly persisted and restored.
2026-01-02 20:34:53 +01:00
7 changed files with 189 additions and 18 deletions

View File

@@ -18,7 +18,7 @@ import (
)
// Version is set at build time via -ldflags, or defaults to dev
var Version = "0.4.0"
var Version = "0.4.3"
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {

View File

@@ -1,6 +1,6 @@
{
"name": "vessel",
"version": "0.4.0",
"version": "0.4.3",
"private": true,
"type": "module",
"scripts": {

View File

@@ -182,6 +182,15 @@
}
});
// Sync custom context limit with settings
$effect(() => {
if (settingsState.useCustomParameters) {
contextManager.setCustomContextLimit(settingsState.num_ctx);
} else {
contextManager.setCustomContextLimit(null);
}
});
// Update context manager when messages change
$effect(() => {
contextManager.updateMessages(chatState.visibleMessages);
@@ -604,12 +613,16 @@
// The results are stored in toolCalls and displayed by ToolCallDisplay
}
// Persist the assistant message (without flooding text content)
// Persist the assistant message (including toolCalls for reload persistence)
if (conversationId && assistantNode) {
const parentOfAssistant = assistantNode.parentId;
await addStoredMessage(
conversationId,
{ role: 'assistant', content: assistantNode.message.content },
{
role: 'assistant',
content: assistantNode.message.content,
toolCalls: assistantNode.message.toolCalls
},
parentOfAssistant,
assistantMessageId
);

View File

@@ -5,6 +5,7 @@
*/
import { settingsState } from '$lib/stores/settings.svelte';
import { modelsState, type ModelDefaults } from '$lib/stores/models.svelte';
import {
PARAMETER_RANGES,
PARAMETER_LABELS,
@@ -16,6 +17,26 @@
// Parameter keys for iteration
const parameterKeys: (keyof ModelParameters)[] = ['temperature', 'top_k', 'top_p', 'num_ctx'];
// Track model defaults for the selected model
let modelDefaults = $state<ModelDefaults>({});
// Fetch model defaults when panel opens or model changes
$effect(() => {
if (settingsState.isPanelOpen && modelsState.selectedId) {
modelsState.fetchModelDefaults(modelsState.selectedId).then((defaults) => {
modelDefaults = defaults;
});
}
});
/**
* Get the default value for a parameter (from model or hardcoded fallback)
*/
function getDefaultValue(key: keyof ModelParameters): number {
const modelValue = modelDefaults[key];
return modelValue ?? DEFAULT_MODEL_PARAMETERS[key];
}
/**
* Format a parameter value for display
*/
@@ -79,7 +100,7 @@
type="button"
role="switch"
aria-checked={settingsState.useCustomParameters}
onclick={() => settingsState.toggleCustomParameters()}
onclick={() => settingsState.toggleCustomParameters(modelDefaults)}
class="relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 focus:ring-offset-theme-secondary {settingsState.useCustomParameters ? 'bg-sky-600' : 'bg-theme-tertiary'}"
>
<span
@@ -93,7 +114,7 @@
{#each parameterKeys as key}
{@const range = PARAMETER_RANGES[key]}
{@const value = getValue(key)}
{@const isDefault = value === DEFAULT_MODEL_PARAMETERS[key]}
{@const isDefault = value === getDefaultValue(key)}
<div>
<div class="mb-1 flex items-center justify-between">
@@ -132,7 +153,7 @@
<div class="mt-4 flex justify-end">
<button
type="button"
onclick={() => settingsState.resetToDefaults()}
onclick={() => settingsState.resetToDefaults(modelDefaults)}
class="rounded px-2 py-1 text-xs text-theme-muted hover:bg-theme-tertiary hover:text-theme-secondary"
>
Reset to defaults

View File

@@ -24,8 +24,14 @@ class ContextManager {
/** Current model name */
currentModel = $state<string>('');
/** Maximum context length for current model */
maxTokens = $state<number>(4096);
/** Maximum context length for current model (from model lookup) */
modelMaxTokens = $state<number>(4096);
/** Custom context limit override (from user settings) */
customMaxTokens = $state<number | null>(null);
/** Effective max tokens (custom override or model default) */
maxTokens = $derived(this.customMaxTokens ?? this.modelMaxTokens);
/**
* Cached token estimates for messages (id -> estimate)
@@ -94,7 +100,15 @@ class ContextManager {
*/
setModel(modelName: string): void {
this.currentModel = modelName;
this.maxTokens = getModelContextLimit(modelName);
this.modelMaxTokens = getModelContextLimit(modelName);
}
/**
* Set custom context limit override
* Pass null to clear and use model default
*/
setCustomContextLimit(tokens: number | null): void {
this.customMaxTokens = tokens;
}
/**

View File

@@ -65,6 +65,14 @@ export interface ModelUpdateStatus {
localModifiedAt: string;
}
/** Model default parameters from Ollama */
export interface ModelDefaults {
temperature?: number;
top_k?: number;
top_p?: number;
num_ctx?: number;
}
/** Models state class with reactive properties */
export class ModelsState {
// Core state
@@ -81,6 +89,10 @@ export class ModelsState {
private capabilitiesCache = $state<Map<string, OllamaCapability[]>>(new Map());
private capabilitiesFetching = new Set<string>();
// Model defaults cache: modelName -> default parameters
private modelDefaultsCache = $state<Map<string, ModelDefaults>>(new Map());
private modelDefaultsFetching = new Set<string>();
// Derived: Currently selected model
selected = $derived.by(() => {
if (!this.selectedId) return null;
@@ -429,6 +441,99 @@ export class ModelsState {
}
return count;
}
// =========================================================================
// Model Defaults
// =========================================================================
/**
* Parse model parameters from the Ollama show response
* The parameters field contains lines like "temperature 0.7" or "num_ctx 4096"
*/
private parseModelParameters(parametersStr: string): ModelDefaults {
const defaults: ModelDefaults = {};
if (!parametersStr) return defaults;
const lines = parametersStr.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
// Parse "key value" format
const spaceIndex = trimmed.indexOf(' ');
if (spaceIndex === -1) continue;
const key = trimmed.substring(0, spaceIndex).toLowerCase();
const value = trimmed.substring(spaceIndex + 1).trim();
switch (key) {
case 'temperature':
defaults.temperature = parseFloat(value);
break;
case 'top_k':
defaults.top_k = parseInt(value, 10);
break;
case 'top_p':
defaults.top_p = parseFloat(value);
break;
case 'num_ctx':
defaults.num_ctx = parseInt(value, 10);
break;
}
}
return defaults;
}
/**
* Fetch model defaults from Ollama /api/show
*/
async fetchModelDefaults(modelName: string): Promise<ModelDefaults> {
// Check cache first
const cached = this.modelDefaultsCache.get(modelName);
if (cached) return cached;
// Avoid duplicate fetches
if (this.modelDefaultsFetching.has(modelName)) {
await new Promise((r) => setTimeout(r, 100));
return this.modelDefaultsCache.get(modelName) ?? {};
}
this.modelDefaultsFetching.add(modelName);
try {
const response = await ollamaClient.showModel(modelName);
const defaults = this.parseModelParameters(response.parameters);
// Update cache reactively
const newCache = new Map(this.modelDefaultsCache);
newCache.set(modelName, defaults);
this.modelDefaultsCache = newCache;
return defaults;
} catch (err) {
console.warn(`Failed to fetch defaults for ${modelName}:`, err);
return {};
} finally {
this.modelDefaultsFetching.delete(modelName);
}
}
/**
* Get cached model defaults (returns empty if not fetched)
*/
getModelDefaults(modelName: string): ModelDefaults {
return this.modelDefaultsCache.get(modelName) ?? {};
}
/**
* Get defaults for selected model
*/
get selectedModelDefaults(): ModelDefaults {
if (!this.selectedId) return {};
return this.modelDefaultsCache.get(this.selectedId) ?? {};
}
}
/** Singleton models state instance */

View File

@@ -10,6 +10,7 @@ import {
DEFAULT_CHAT_SETTINGS,
PARAMETER_RANGES
} from '$lib/types/settings';
import type { ModelDefaults } from './models.svelte';
const STORAGE_KEY = 'vessel-settings';
@@ -79,12 +80,30 @@ export class SettingsState {
/**
* Toggle whether to use custom parameters
* When enabling, optionally initialize from model defaults
*/
toggleCustomParameters(): void {
toggleCustomParameters(modelDefaults?: ModelDefaults): void {
this.useCustomParameters = !this.useCustomParameters;
// When enabling custom parameters, initialize from model defaults if provided
if (this.useCustomParameters && modelDefaults) {
this.initializeFromModelDefaults(modelDefaults);
}
this.saveToStorage();
}
/**
* Initialize parameters from model defaults
* Falls back to hardcoded defaults for any missing values
*/
initializeFromModelDefaults(modelDefaults: ModelDefaults): void {
this.temperature = modelDefaults.temperature ?? DEFAULT_MODEL_PARAMETERS.temperature;
this.top_k = modelDefaults.top_k ?? DEFAULT_MODEL_PARAMETERS.top_k;
this.top_p = modelDefaults.top_p ?? DEFAULT_MODEL_PARAMETERS.top_p;
this.num_ctx = modelDefaults.num_ctx ?? DEFAULT_MODEL_PARAMETERS.num_ctx;
}
/**
* Update a single parameter
*/
@@ -112,14 +131,13 @@ export class SettingsState {
}
/**
* Reset all parameters to defaults
* Reset all parameters to model defaults (or hardcoded defaults if not available)
*/
resetToDefaults(): void {
this.temperature = DEFAULT_MODEL_PARAMETERS.temperature;
this.top_k = DEFAULT_MODEL_PARAMETERS.top_k;
this.top_p = DEFAULT_MODEL_PARAMETERS.top_p;
this.num_ctx = DEFAULT_MODEL_PARAMETERS.num_ctx;
this.useCustomParameters = false;
resetToDefaults(modelDefaults?: ModelDefaults): void {
this.temperature = modelDefaults?.temperature ?? DEFAULT_MODEL_PARAMETERS.temperature;
this.top_k = modelDefaults?.top_k ?? DEFAULT_MODEL_PARAMETERS.top_k;
this.top_p = modelDefaults?.top_p ?? DEFAULT_MODEL_PARAMETERS.top_p;
this.num_ctx = modelDefaults?.num_ctx ?? DEFAULT_MODEL_PARAMETERS.num_ctx;
this.saveToStorage();
}