From 201d466adb3af31fc7cdbba82aa1659f5b2da778 Mon Sep 17 00:00:00 2001 From: Matyas Fenyves <16389204+uhhgoat@users.noreply.github.com> Date: Thu, 11 Jun 2026 21:33:16 +0200 Subject: [PATCH] feat(chat): add model control defaults --- routes/auth_routes.py | 12 ++++++++++ routes/model_routes.py | 31 +++++++++++++++++++++++-- src/settings.py | 3 +++ static/app.js | 51 +++++++++++++++++++++++++++++++++++++++++- static/index.html | 28 +++++++++++++++++++++++ static/js/chat.js | 10 ++++++++- static/js/sessions.js | 45 +++++++++++++++++++++++++++++++------ static/js/settings.js | 41 ++++++++++++++++++++++++++++++++- 8 files changed, 209 insertions(+), 12 deletions(-) diff --git a/routes/auth_routes.py b/routes/auth_routes.py index 5c7a4e04a..c8db2dbaf 100644 --- a/routes/auth_routes.py +++ b/routes/auth_routes.py @@ -656,6 +656,10 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter: "agent_max_rounds": (1, 200), "agent_max_tool_calls": (0, 1000), # 0 = unlimited } + _CHOICE_VALUES = { + "default_reasoning_effort": {"", "auto", "off", "on", "none", "minimal", "low", "medium", "high", "xhigh"}, + "default_verbosity": {"", "auto", "low", "medium", "high"}, + } for key in DEFAULT_SETTINGS: if key not in body: continue @@ -667,6 +671,14 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter: except (TypeError, ValueError): raise HTTPException(400, f"{key} must be an integer") val = max(lo, min(val, hi)) + if key in _CHOICE_VALUES: + val = str(val or "").strip().lower().replace("-", "_") + if val == "x_high": + val = "xhigh" + if val in ("auto", "default"): + val = "" + if val not in _CHOICE_VALUES[key]: + raise HTTPException(400, f"Unsupported value for {key}") current[key] = val _save_settings(current) return current diff --git a/routes/model_routes.py b/routes/model_routes.py index 600150a66..d6472812a 100644 --- a/routes/model_routes.py +++ b/routes/model_routes.py @@ -2438,6 +2438,8 @@ def setup_model_routes(model_discovery): ep_id = (_user_prefs.get("default_endpoint_id") or "").strip() model = (_user_prefs.get("default_model") or "").strip() _fallbacks = _user_prefs.get("default_model_fallbacks") or [] + default_reasoning_effort = (_user_prefs.get("default_reasoning_effort") or "").strip() + default_verbosity = (_user_prefs.get("default_verbosity") or "").strip() # If user has no personal default, fall back to global default # But only based on the "share_defaults_with_users" flag # (only if share_defaults_with_users is enabled) @@ -2448,10 +2450,35 @@ def setup_model_routes(model_discovery): model = settings.get("default_model", "") if not _fallbacks: _fallbacks = settings.get("default_model_fallbacks") or [] + if not default_reasoning_effort: + default_reasoning_effort = (settings.get("default_reasoning_effort") or "").strip() + if not default_verbosity: + default_verbosity = (settings.get("default_verbosity") or "").strip() else: ep_id = settings.get("default_endpoint_id", "") model = settings.get("default_model", "") _fallbacks = settings.get("default_model_fallbacks") or [] + default_reasoning_effort = (settings.get("default_reasoning_effort") or "").strip() + default_verbosity = (settings.get("default_verbosity") or "").strip() + + def _clean_default_reasoning(value: str) -> str: + cleaned = (value or "").strip().lower().replace("-", "_") + if cleaned in {"", "auto", "default"}: + return "" + if cleaned == "none": + return "off" + return cleaned if cleaned in {"off", "on", "minimal", "low", "medium", "high", "xhigh"} else "" + + def _clean_default_verbosity(value: str) -> str: + cleaned = (value or "").strip().lower() + if cleaned in {"", "auto", "default"}: + return "" + return cleaned if cleaned in {"low", "medium", "high"} else "" + + default_controls = { + "default_reasoning_effort": _clean_default_reasoning(default_reasoning_effort), + "default_verbosity": _clean_default_verbosity(default_verbosity), + } db = SessionLocal() try: ep = None @@ -2504,7 +2531,7 @@ def setup_model_routes(model_discovery): _last_q = owner_filter(_last_q, ModelEndpoint, _user, include_shared=False) ep = _last_q.first() if not ep: - return {"endpoint_id": "", "endpoint_url": "", "model": ""} + return {"endpoint_id": "", "endpoint_url": "", "model": "", **default_controls} base = _normalize_base(ep.base_url) chat_url = build_chat_url(base) if not model and (getattr(ep, "cached_models", None) or getattr(ep, "pinned_models", None)): @@ -2514,7 +2541,7 @@ def setup_model_routes(model_discovery): model = visible[0] except Exception: pass - return {"endpoint_id": ep.id, "endpoint_url": chat_url, "model": model} + return {"endpoint_id": ep.id, "endpoint_url": chat_url, "model": model, **default_controls} finally: db.close() diff --git a/src/settings.py b/src/settings.py index 5836765f1..16839efaf 100644 --- a/src/settings.py +++ b/src/settings.py @@ -134,6 +134,8 @@ DEFAULT_SETTINGS = { "task_model": "", "default_endpoint_id": "", "default_model": "", + "default_reasoning_effort": "", + "default_verbosity": "", # Optional prose style used only for normal document writing/editing. # Email replies use email_writing_style instead because greetings, # signatures, and mailbox identity rules are medium-specific. @@ -271,6 +273,7 @@ _PER_USER_KEYS = { # account inherited whatever the most-recent admin picked, which then # got injected into the chat composer on first open. "default_endpoint_id", "default_model", "default_model_fallbacks", + "default_reasoning_effort", "default_verbosity", "utility_endpoint_id", "utility_model", "utility_model_fallbacks", "research_endpoint_id", "research_model", } diff --git a/static/app.js b/static/app.js index bac07392a..14db8905d 100644 --- a/static/app.js +++ b/static/app.js @@ -209,6 +209,10 @@ try { if (cachedDefaultChat && cachedDefaultChat.endpoint_url && cachedDefaultChat.model) { _defaultChat = cachedDefaultChat; window.__odysseusDefaultChat = cachedDefaultChat; + window.__odysseusModelControlDefaults = { + reasoning_effort: cachedDefaultChat.default_reasoning_effort || 'auto', + verbosity: cachedDefaultChat.default_verbosity || 'auto', + }; } } catch (_) {} async function _refreshDefaultChat() { @@ -218,6 +222,10 @@ async function _refreshDefaultChat() { _defaultChat = d; try { window.__odysseusDefaultChat = d; + window.__odysseusModelControlDefaults = { + reasoning_effort: d.default_reasoning_effort || 'auto', + verbosity: d.default_verbosity || 'auto', + }; localStorage.setItem('odysseus-default-chat-cache', JSON.stringify(d)); } catch (_) {} return d; @@ -248,7 +256,11 @@ async function _createDirectChatFromPreferredModel() { const dc = await _refreshDefaultChat(); if (dc) { - sessionModule.createDirectChat(dc.endpoint_url, dc.model, dc.endpoint_id, { source: 'default' }); + sessionModule.createDirectChat(dc.endpoint_url, dc.model, dc.endpoint_id, { + source: 'default', + reasoning_effort: dc.default_reasoning_effort || '', + verbosity: dc.default_verbosity || '', + }); return true; } @@ -1991,6 +2003,29 @@ function initializeEventListeners() { const v = String(value || 'auto').toLowerCase(); return Object.prototype.hasOwnProperty.call(labels, v) ? v : 'auto'; }; + const normalizeDefaultControl = (key, value) => { + let normalized = String(value || 'auto').toLowerCase().replace(/-/g, '_'); + if (normalized === 'none') normalized = 'off'; + const config = controls.find(c => c.key === key); + return config ? normalizeValue(normalized, config.labels) : 'auto'; + }; + let modelControlDefaults = { + reasoning_effort: 'auto', + verbosity: 'auto', + }; + function setModelControlDefaults(values = {}) { + modelControlDefaults = { + reasoning_effort: normalizeDefaultControl( + 'reasoning_effort', + values.reasoning_effort || values.default_reasoning_effort || 'auto', + ), + verbosity: normalizeDefaultControl( + 'verbosity', + values.verbosity || values.default_verbosity || 'auto', + ), + }; + try { window.__odysseusModelControlDefaults = { ...modelControlDefaults }; } catch (_) {} + } const isThinkingModel = model => { const m = String(model || '').toLowerCase(); @@ -2216,6 +2251,7 @@ function initializeEventListeners() { }); refreshCapabilities(); + setModelControlDefaults(window.__odysseusModelControlDefaults || window.__odysseusDefaultChat || {}); window.odysseusModelControls = { applySession(meta = {}) { @@ -2227,9 +2263,22 @@ function initializeEventListeners() { } refreshCapabilities(); }, + getDefaults() { + return { ...modelControlDefaults }; + }, + setDefaults(values = {}) { + setModelControlDefaults(values); + }, refreshCapabilities, }; + fetch('/api/auth/settings', { credentials: 'same-origin' }) + .then(res => res.ok ? res.json() : null) + .then(settings => { + if (settings) setModelControlDefaults(settings); + }) + .catch(() => {}); + document.addEventListener('odysseus:model-picked', e => { const detail = (e && e.detail) || {}; refreshCapabilities({ model: detail.mid, endpointUrl: detail.url }); diff --git a/static/index.html b/static/index.html index 441512177..7db8b179a 100644 --- a/static/index.html +++ b/static/index.html @@ -1523,6 +1523,34 @@
+