From cb3c4888c4fc7c602da3932136dbf62cbe9964a4 Mon Sep 17 00:00:00 2001 From: Matyas Fenyves <16389204+uhhgoat@users.noreply.github.com> Date: Fri, 12 Jun 2026 08:27:18 +0200 Subject: [PATCH] feat(chat): apply defaults on model switch --- static/app.js | 65 ++++++++++++++++++++++++++++++++++++-- static/js/modelPicker.js | 17 ++++++++++ static/js/sessions.js | 67 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 141 insertions(+), 8 deletions(-) diff --git a/static/app.js b/static/app.js index 14db8905d..7597c4ece 100644 --- a/static/app.js +++ b/static/app.js @@ -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, }; diff --git a/static/js/modelPicker.js b/static/js/modelPicker.js index 85e72d07d..8c662bcf6 100644 --- a/static/js/modelPicker.js +++ b/static/js/modelPicker.js @@ -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); diff --git a/static/js/sessions.js b/static/js/sessions.js index 5327b331f..2573a225c 100644 --- a/static/js/sessions.js +++ b/static/js/sessions.js @@ -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();