feat(chat): apply defaults on model switch

This commit is contained in:
Matyas Fenyves 2026-06-12 08:27:18 +02:00
parent 201d466adb
commit cb3c4888c4
3 changed files with 141 additions and 8 deletions

View file

@ -2027,6 +2027,16 @@ function initializeEventListeners() {
try { window.__odysseusModelControlDefaults = { ...modelControlDefaults }; } catch (_) {}
}
async function refreshModelControlDefaults() {
try {
const res = await fetch('/api/auth/settings', { credentials: 'same-origin' });
if (!res.ok) return { ...modelControlDefaults };
const settings = await res.json();
if (settings) setModelControlDefaults(settings);
} catch (_) {}
return { ...modelControlDefaults };
}
const isThinkingModel = model => {
const m = String(model || '').toLowerCase();
return ['qwen3', 'qwq', 'deepseek-r1', 'deepseek-reasoner', 'minimax', 'm2-reap', 'gemma']
@ -2108,8 +2118,8 @@ function initializeEventListeners() {
return { supported: false, allowed: new Set(['auto']), reason: '' };
}
async function persistSessionControl(key, value) {
const sid = sessionModule && sessionModule.getCurrentSessionId ? sessionModule.getCurrentSessionId() : null;
async function persistSessionControl(key, value, sessionId = null) {
const sid = sessionId || (sessionModule && sessionModule.getCurrentSessionId ? sessionModule.getCurrentSessionId() : null);
if (!sid) return;
const fd = new FormData();
fd.append(key, value || 'auto');
@ -2145,6 +2155,52 @@ function initializeEventListeners() {
Object.values(registry).forEach(api => api.refreshCapability(override));
}
function resolveControlForContext(key, value, contextOverride = null) {
const config = controls.find(item => item.key === key);
if (!config) return 'auto';
const capability = capabilitiesFor(key, contextOverride);
const normalized = normalizeDefaultControl(key, value);
return capability.allowed.has(normalized) ? normalized : 'auto';
}
function resolveDefaultsForContext(contextOverride = null) {
return {
reasoning_effort: resolveControlForContext(
'reasoning_effort',
modelControlDefaults.reasoning_effort,
contextOverride,
),
verbosity: resolveControlForContext(
'verbosity',
modelControlDefaults.verbosity,
contextOverride,
),
};
}
async function applyDefaultsForContext(contextOverride = null, options = {}) {
if (options.refresh !== false) {
await refreshModelControlDefaults();
}
const resolved = resolveDefaultsForContext(contextOverride);
if (registry.reasoning_effort) {
registry.reasoning_effort.setValue(resolved.reasoning_effort, {
persist: options.persist !== false,
contextOverride,
sessionId: options.sessionId || null,
});
}
if (registry.verbosity) {
registry.verbosity.setValue(resolved.verbosity, {
persist: options.persist !== false,
contextOverride,
sessionId: options.sessionId || null,
});
}
refreshCapabilities(contextOverride);
return resolved;
}
controls.forEach(config => {
const btn = el(config.btnId);
const menu = el(config.menuId);
@ -2172,7 +2228,7 @@ function initializeEventListeners() {
opt.classList.toggle('active', opt.dataset.value === normalized);
});
if (optionsArg.persist !== false) {
persistSessionControl(config.key, normalized);
persistSessionControl(config.key, normalized, optionsArg.sessionId || null);
}
}
@ -2266,9 +2322,12 @@ function initializeEventListeners() {
getDefaults() {
return { ...modelControlDefaults };
},
resolveDefaultsForContext,
applyDefaultsForContext,
setDefaults(values = {}) {
setModelControlDefaults(values);
},
refreshDefaults: refreshModelControlDefaults,
refreshCapabilities,
};

View file

@ -272,6 +272,20 @@ function _initModelPickerDropdown() {
} catch (_) { /* leave stale data; picker still works */ }
}
async function _applyModelControlDefaults(m, persist, sessionId) {
if (!m || !window.odysseusModelControls || typeof window.odysseusModelControls.applyDefaultsForContext !== 'function') {
return;
}
try {
await window.odysseusModelControls.applyDefaultsForContext(
{ model: m.mid || '', endpointUrl: m.url || '' },
{ persist: !!persist, sessionId: sessionId || null },
);
} catch (e) {
console.warn('Model picker: failed to apply model control defaults', e);
}
}
function _getAllModels() {
const items = (window.modelsModule && window.modelsModule.getCachedItems) ? window.modelsModule.getCachedItems() : [];
const result = [];
@ -683,6 +697,7 @@ async function _pick(m) {
if (!currentSessionId && _pendingChat) {
// Already have a deferred session — just update the model
_deps.setPendingChat({ url: m.url, modelId: m.mid, endpointId: m.endpointId, source: 'manual' });
await _applyModelControlDefaults(m, false);
// Header stays as session name — model switch only updates picker
updateModelPicker();
uiModule.showToast(`Using ${m.display}`);
@ -692,6 +707,7 @@ async function _pick(m) {
// No session yet — create one with this model
try {
await _deps.createDirectChat(m.url, m.mid, m.endpointId);
await _applyModelControlDefaults(m, false);
} catch (e) {
uiModule.showError('Failed to start chat: ' + e);
finishSwitch();
@ -714,6 +730,7 @@ async function _pick(m) {
finishSwitch();
return;
}
await _applyModelControlDefaults(m, true, currentSessionId);
// Header stays as session name — model info shown in picker only
} catch (e) {
uiModule.showError('Failed to set model: ' + e);

View file

@ -2192,12 +2192,56 @@ async function _getPreferredDefaultChat() {
return null;
}
function _normalizePendingReasoning(value) {
let v = String(value || '').trim().toLowerCase().replace(/-/g, '_');
if (v === 'x_high') v = 'xhigh';
if (v === 'none') v = 'off';
return ['off', 'on', 'minimal', 'low', 'medium', 'high', 'xhigh'].includes(v) ? v : '';
}
function _normalizePendingVerbosity(value) {
const v = String(value || '').trim().toLowerCase();
return ['low', 'medium', 'high'].includes(v) ? v : '';
}
function _resolveInitialModelControls(modelControls) {
const defaults = (window.odysseusModelControls && window.odysseusModelControls.getDefaults)
? window.odysseusModelControls.getDefaults()
: (window.__odysseusModelControlDefaults || {});
const source = modelControls || defaults || {};
return {
reasoning_effort: _normalizePendingReasoning(source.reasoning_effort || source.default_reasoning_effort || ''),
verbosity: _normalizePendingVerbosity(source.verbosity || source.default_verbosity || ''),
};
}
function _hasExplicitModelControls(modelControls) {
if (!modelControls) return false;
return Object.prototype.hasOwnProperty.call(modelControls, 'reasoning_effort')
|| Object.prototype.hasOwnProperty.call(modelControls, 'default_reasoning_effort')
|| Object.prototype.hasOwnProperty.call(modelControls, 'verbosity')
|| Object.prototype.hasOwnProperty.call(modelControls, 'default_verbosity');
}
function _defaultsForPendingModel(pending) {
if (
window.odysseusModelControls
&& typeof window.odysseusModelControls.resolveDefaultsForContext === 'function'
) {
return window.odysseusModelControls.resolveDefaultsForContext({
model: (pending && pending.modelId) || '',
endpointUrl: (pending && pending.url) || '',
});
}
return _resolveInitialModelControls(null);
}
export function createDirectChat(url, modelId, endpointId, opts = {}) {
const incomingSource = opts.source || 'manual';
const initialControls = {
reasoning_effort: opts.reasoning_effort || opts.default_reasoning_effort || '',
verbosity: opts.verbosity || opts.default_verbosity || '',
};
const defaults = _defaultsForPendingModel({ url, modelId, endpointId });
const initialControls = _resolveInitialModelControls(
_hasExplicitModelControls(opts) ? opts : defaults,
);
if (
_pendingChat &&
_pendingChat.modelId &&
@ -2822,7 +2866,20 @@ function _initAllDropdowns() {
getCurrentSessionId: () => currentSessionId,
getSessions: () => sessions,
getPendingChat: () => _pendingChat,
setPendingChat: (v) => { _pendingChat = v; },
setPendingChat: (v) => {
const controls = _hasExplicitModelControls(v)
? _resolveInitialModelControls(v)
: _resolveInitialModelControls(_defaultsForPendingModel(v));
_pendingChat = v ? { ...v, ...controls } : v;
if (_pendingChat && window.odysseusModelControls && window.odysseusModelControls.applySession) {
window.odysseusModelControls.applySession({
model: _pendingChat.modelId || '',
endpoint_url: _pendingChat.url || '',
reasoning_effort: _pendingChat.reasoning_effort || null,
verbosity: _pendingChat.verbosity || null,
});
}
},
createDirectChat,
});
_initDropdownDismiss();