From d397b3db2f93d36068a96e9524f8b6bfa4f09f0e Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Tue, 9 Jun 2026 10:31:43 +0900 Subject: [PATCH] Restore dropped regression fixes --- mcp_servers/email_server.py | 4 ++-- routes/cookbook_routes.py | 6 +++++ routes/model_routes.py | 21 +++++++++++++++++ static/js/cookbook.js | 45 ++++++++++++++++++++++++++++--------- static/js/cookbookServe.js | 15 ++++++------- 5 files changed, 70 insertions(+), 21 deletions(-) diff --git a/mcp_servers/email_server.py b/mcp_servers/email_server.py index db731ec0f..b807937cd 100644 --- a/mcp_servers/email_server.py +++ b/mcp_servers/email_server.py @@ -1130,8 +1130,8 @@ def _create_email_draft_document( def _draft_reply_to_email(uid, body, folder="INBOX", reply_all=False, account=None, title=None): """Create a threaded Odysseus reply draft document. Does not send.""" conn = _imap_connect(account) - conn.select(folder, readonly=True) - status, msg_data = conn.uid("FETCH", _b(uid), "(RFC822)") + conn.select(_q(folder), readonly=True) + status, msg_data = conn.uid("FETCH", _b(uid), "(BODY.PEEK[])") conn.logout() if status != "OK" or not msg_data or not msg_data[0]: return {"error": f"Failed to fetch email UID {uid}"} diff --git a/routes/cookbook_routes.py b/routes/cookbook_routes.py index ba950f4b7..081638cae 100644 --- a/routes/cookbook_routes.py +++ b/routes/cookbook_routes.py @@ -1219,6 +1219,12 @@ def setup_cookbook_routes() -> APIRouter: # pip cache so they don't fail mid-build with "No space left" (#1219) # and leave the dep installed-but-unusable (#1459). req.cmd = _pip_install_no_cache(req.cmd) + # Accept common aliases and enforce server extras for llama-cpp so + # `python -m llama_cpp.server` has all runtime dependencies. + req.cmd = re.sub(r"(?=!~,` for version specifiers. # v2 review HIGH-14: tightened from the previous regex which diff --git a/routes/model_routes.py b/routes/model_routes.py index 133758e82..864035884 100644 --- a/routes/model_routes.py +++ b/routes/model_routes.py @@ -503,6 +503,24 @@ def _is_chat_model(model_id: str) -> bool: return True +def _delete_orphaned_provider_auth(db, auth_id: Optional[str], exclude_ep_id: Optional[str] = None) -> bool: + """Delete a ProviderAuthSession once no endpoint still references it.""" + if not auth_id: + return False + from core.database import ProviderAuthSession + still_referenced = db.query(ModelEndpoint.id).filter( + ModelEndpoint.provider_auth_id == auth_id, + ModelEndpoint.id != exclude_ep_id, + ).first() + if still_referenced is not None: + return False + auth_row = db.query(ProviderAuthSession).filter(ProviderAuthSession.id == auth_id).first() + if auth_row is None: + return False + db.delete(auth_row) + return True + + def _safe_detect_provider(base_url: str) -> str: """Best-effort provider detection that must not break endpoint probing.""" try: @@ -2173,7 +2191,9 @@ def setup_model_routes(model_discovery): cleared_user_preferences = _clear_user_prefs_for_endpoint(ep_id) cleared_sessions = _clear_sessions_for_endpoint(db, ep.base_url) cleared_loaded_sessions = _clear_loaded_sessions_for_endpoint(ep.base_url) + auth_id = getattr(ep, "provider_auth_id", None) db.delete(ep) + cleared_provider_auth = _delete_orphaned_provider_auth(db, auth_id, exclude_ep_id=ep_id) db.commit() _invalidate_models_cache() _local_probe_cache["data"] = None @@ -2183,6 +2203,7 @@ def setup_model_routes(model_discovery): "cleared_user_preferences": cleared_user_preferences, "cleared_sessions": cleared_sessions, "cleared_loaded_sessions": cleared_loaded_sessions, + "cleared_provider_auth": cleared_provider_auth, } finally: db.close() diff --git a/static/js/cookbook.js b/static/js/cookbook.js index 43a0f9d5d..2abb263ba 100644 --- a/static/js/cookbook.js +++ b/static/js/cookbook.js @@ -117,7 +117,7 @@ function _isLocalEntry(s) { return !s || !s.host || s.host === 'local' || s.host // Resolve a dropdown option value to a server entry. New option values are // stable per-profile keys, so same-host SSH profiles stay distinguishable. // Host strings and numeric indices remain accepted for stale saved state. -function _serverKey(s) { +export function _serverKey(s) { if (_isLocalEntry(s)) return 'local'; return 'srv:' + [ s?.name || '', @@ -128,7 +128,7 @@ function _serverKey(s) { ].map(v => encodeURIComponent(String(v).trim())).join('|'); } -function _serverByVal(val) { +export function _serverByVal(val) { if (val == null || val === 'local' || val === '') return null; const raw = String(val); let s = _envState.servers.find(x => _serverKey(x) === raw); @@ -138,7 +138,7 @@ function _serverByVal(val) { return s || null; } -function _selectedServer() { +export function _selectedServer() { if (_envState.remoteServerKey) { const keyed = _serverByVal(_envState.remoteServerKey); if (keyed) return keyed; @@ -147,12 +147,25 @@ function _selectedServer() { return null; } -function _currentServerValue() { +export function _currentServerValue() { const selected = _selectedServer(); if (selected) return _serverKey(selected); return _envState.remoteHost || 'local'; } +const GEMMA4_THINKING_CHAT_TEMPLATE = `{% for message in messages %}{% if message['role'] == 'system' %}<|turn>system\n<|think|>{{ message['content'] }}\n{% elif message['role'] == 'user' %}<|turn>user\n{{ message['content'] }}\n{% elif message['role'] == 'assistant' %}<|turn>model\n{{ message['content'] }}\n{% endif %}{% endfor %}{% if add_generation_prompt %}<|turn>model\n<|channel>thought{% endif %}`; + +function _isGemma4ThinkingModel(modelName) { + const n = (modelName || '').toLowerCase(); + return n.includes('gemma-4') || n.includes('gemma4'); +} + +function _gemma4ThinkingChatTemplateArg(modelName) { + return _isGemma4ThinkingModel(modelName) + ? _shellQuote(GEMMA4_THINKING_CHAT_TEMPLATE) + : ''; +} + function _buildServerOpts(excludeLocal = false) { // The local server is ALWAYS represented by the synthetic value="local" option // (showing its custom name from the "server name" feature). We must therefore @@ -188,16 +201,18 @@ export function _sshCmd(host, cmd, port) { /** Get SSH port for a given host (or task object) */ function _getPort(hostOrTask) { if (!hostOrTask) return ''; - if (typeof hostOrTask === 'object') return hostOrTask.sshPort || _getPort(hostOrTask.remoteHost); - const srv = _envState.servers.find(s => s.host === hostOrTask); + if (typeof hostOrTask === 'object') return hostOrTask.sshPort || _getPort(hostOrTask.remoteServerKey || hostOrTask.remoteHost); + const selected = hostOrTask === _envState.remoteHost ? _selectedServer() : null; + const srv = selected || _serverByVal(hostOrTask); return srv?.port || ''; } /** Get platform for a given host (or task object). Returns 'windows', 'termux', 'linux', or '' */ export function _getPlatform(hostOrTask) { if (!hostOrTask) return _envState.platform || ''; - if (typeof hostOrTask === 'object') return hostOrTask.platform || _getPlatform(hostOrTask.remoteHost); - const srv = _envState.servers.find(s => s.host === hostOrTask); + if (typeof hostOrTask === 'object') return hostOrTask.platform || _getPlatform(hostOrTask.remoteServerKey || hostOrTask.remoteHost); + const selected = hostOrTask === _envState.remoteHost ? _selectedServer() : null; + const srv = selected || _serverByVal(hostOrTask); return srv?.platform || ''; } @@ -416,6 +431,8 @@ export function _buildServeCmd(f, modelName, backend) { const _extraEnv = (f.extra_env ?? '').toString().replace(/\s+/g, ' ').trim(); if (_extraEnv) cmd += _extraEnv + ' '; cmd += `${_vllmBin} serve ${modelName} --host 0.0.0.0 --port ${f.port || '8000'}`; + const _gemma4ChatTemplate = _gemma4ThinkingChatTemplateArg(modelName); + if (_gemma4ChatTemplate) cmd += ` --chat-template ${_gemma4ChatTemplate}`; cmd += ` --tensor-parallel-size ${f.tp || '1'}`; cmd += ` --max-model-len ${f.ctx || '8192'}`; cmd += ` --gpu-memory-utilization ${f.gpu_mem || '0.90'}`; @@ -446,6 +463,8 @@ export function _buildServeCmd(f, modelName, backend) { const _extraEnv = (f.extra_env ?? '').toString().replace(/\s+/g, ' ').trim(); if (_extraEnv) cmd += _extraEnv + ' '; cmd += `${_py3Bin} -m sglang.launch_server --model-path ${modelName} --host 0.0.0.0 --port ${f.port || '30000'}`; + const _gemma4ChatTemplate = _gemma4ThinkingChatTemplateArg(modelName); + if (_gemma4ChatTemplate) cmd += ` --chat-template ${_gemma4ChatTemplate}`; if (f.tp && f.tp !== '1') cmd += ` --tp ${f.tp}`; if (f.ctx) cmd += ` --context-length ${f.ctx}`; if (f.gpu_mem && f.gpu_mem !== '0.90') cmd += ` --mem-fraction-static ${f.gpu_mem}`; @@ -910,6 +929,7 @@ async function _fetchDependencies() { function _applyServerSelection(val) { if (val === 'local') { _envState.remoteHost = ''; + _envState.remoteServerKey = ''; _envState.env = 'none'; _envState.envPath = ''; _envState.platform = ''; @@ -917,6 +937,7 @@ function _applyServerSelection(val) { const s = _serverByVal(val); if (s) { _envState.remoteHost = s.host; + _envState.remoteServerKey = _serverKey(s); _envState.env = s.env || 'none'; _envState.envPath = s.envPath || ''; _envState.platform = s.platform || ''; @@ -927,7 +948,7 @@ function _applyServerSelection(val) { // bug: the Download/Cache/Deps dropdowns set the host but never saved it, so // it silently reverted and downloads/scans hit the wrong server). _persistEnvState(); - const _want = _envState.remoteHost || 'local'; + const _want = _currentServerValue(); document.querySelectorAll('#hwfit-server-select, #hwfit-dl-server, #hwfit-cache-server, #hwfit-deps-server').forEach(sel => { if (!sel || sel.tagName !== 'SELECT') return; // Option values are host strings now ('local' for the local box). @@ -1038,7 +1059,7 @@ function _wireTabEvents(body) { // UI matches the resolved host. Done in a microtask so the dropdowns // exist by the time we set their .value. Promise.resolve().then(() => { - const _want = _envState.remoteHost || 'local'; + const _want = _currentServerValue(); document.querySelectorAll('#hwfit-server-select, #hwfit-dl-server, #hwfit-cache-server, #hwfit-deps-server').forEach(sel => { if (sel && sel.tagName === 'SELECT') sel.value = _want; }); @@ -2265,6 +2286,8 @@ const shared = { _sshCmd, _getPort, _sshPrefix, + _serverByVal, + _selectedServer, _getPlatform, _isWindows, _isMetal, @@ -2317,7 +2340,7 @@ export { _startBackgroundMonitor, _setPanelField, _setPanelCheckbox, _wirePanelEvents, _runPanelCmd, _runModelDownload, _buildDownloadCmd, - _serverByVal, _serverKey, _currentServerValue, _selectedServer, _isLocalEntry, + _isLocalEntry, }; const cookbookModule = { open, close, isVisible, startBackgroundMonitor: _startBackgroundMonitor }; diff --git a/static/js/cookbookServe.js b/static/js/cookbookServe.js index d06477baf..28aee1380 100644 --- a/static/js/cookbookServe.js +++ b/static/js/cookbookServe.js @@ -97,14 +97,14 @@ function _selectedServeTarget(panel) { const select = document.getElementById('hwfit-server-select') || document.getElementById('hwfit-dl-server'); const servers = Array.isArray(_envState.servers) ? _envState.servers : []; let host = _envState.remoteHost || ''; - let server = host ? servers.find(s => s.host === host) : null; + let server = host ? (_serverByVal?.(_envState.remoteServerKey || host) || servers.find(s => s.host === host)) : null; if (select && select.value != null) { if (select.value === 'local') { host = ''; server = servers.find(s => !s.host || s.host === 'local') || null; } else { const idx = /^\d+$/.test(String(select.value)) ? parseInt(select.value, 10) : -1; - server = servers.find(s => s.host === select.value) || (idx >= 0 ? servers[idx] : null) || null; + server = _serverByVal?.(select.value) || (idx >= 0 ? servers[idx] : null) || null; host = server?.host || ''; } } @@ -512,7 +512,7 @@ function _rerenderCachedModels() { // The venv set per-server in Settings (server.envPath). Used as the venv // field default when the global active env path isn't carrying it, so a // configured server venv shows up without re-typing it. - const _selSrv = (_es.servers || []).find(s => s.host === (_es.remoteHost || '')) || {}; + const _selSrv = _serverByVal?.(_es.remoteServerKey || _es.remoteHost || '') || {}; const _srvVenv = _selSrv.envPath || ''; // Serve state schema: { _byRepo: { : {...} }, _lastUsed: {...} }. // Loading priority: this-repo's saved settings → last-used (from any @@ -1771,7 +1771,7 @@ function _rerenderCachedModels() { const _probeParams = new URLSearchParams(); if (_probeHost) { _probeParams.set('host', _probeHost); - const _sp = (_envState.servers || []).find(s => s.host === _probeHost)?.port; + const _sp = (_serverByVal?.(_envState.remoteServerKey || _probeHost) || {}).port; if (_sp) _probeParams.set('ssh_port', _sp); } const _probeRes = await fetch('/api/cookbook/gpus' + (_probeParams.toString() ? '?' + _probeParams : ''), { credentials: 'same-origin' }); @@ -1871,8 +1871,7 @@ function _rerenderCachedModels() { if (_ssEl && _ssEl.value != null) { if (_ssEl.value === 'local') serveHost = ''; else { - // Values are host strings now; resolve by host (numeric fallback). - const _srv = _envState.servers.find(s => s.host === _ssEl.value) || _envState.servers[parseInt(_ssEl.value)]; + const _srv = _serverByVal?.(_ssEl.value) || _envState.servers[parseInt(_ssEl.value)]; if (_srv) { serveHost = _srv.host; _srvEnv = _srv.env || ''; @@ -1931,7 +1930,7 @@ function _resolveCacheHost() { function _serverByCacheValue(val) { if (val === 'local') return null; - const found = _envState.servers.find(x => x.host === val) + const found = _serverByVal?.(val) || (/^\d+$/.test(String(val)) ? _envState.servers[parseInt(val)] : null) || _envState.servers.find(x => x.name === val) || null; @@ -2151,7 +2150,7 @@ export async function _fetchCachedModels() { let selectedServer = null; const _serverByCacheValue = (val) => { if (val === 'local') return null; - return _envState.servers.find(x => x.host === val) + return _serverByVal?.(val) || (/^\d+$/.test(String(val)) ? _envState.servers[parseInt(val)] : null) || _envState.servers.find(x => x.name === val) || null;