mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-23 21:25:33 -04:00
feat: Allow admins to choose if they want to share defaults (#4752)
* First bare fix * Adding the option toggle * toggle function fix * Final fix, added missing /auth/ * Extended toggle text & added tests * Comments change * Description toggle change * br tag fix * description change based on suggestion
This commit is contained in:
@@ -2108,6 +2108,16 @@ 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 []
|
||||
# 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)
|
||||
if settings.get("share_defaults_with_users", False):
|
||||
if not ep_id:
|
||||
ep_id = settings.get("default_endpoint_id", "")
|
||||
if not model:
|
||||
model = settings.get("default_model", "")
|
||||
if not _fallbacks:
|
||||
_fallbacks = settings.get("default_model_fallbacks") or []
|
||||
else:
|
||||
ep_id = settings.get("default_endpoint_id", "")
|
||||
model = settings.get("default_model", "")
|
||||
|
||||
@@ -141,6 +141,10 @@ DEFAULT_SETTINGS = {
|
||||
# before producing output (endpoint offline / errors), the chat
|
||||
# dispatch retries the next entry in order.
|
||||
"default_model_fallbacks": [],
|
||||
# When True, non-admin users inherit global default model/endpoint/fallbacks
|
||||
# when they have no personal defaults. When False, users only use their
|
||||
# personal defaults (no global fallback). Default is False.
|
||||
"share_defaults_with_users": False,
|
||||
"utility_endpoint_id": "",
|
||||
"utility_model": "",
|
||||
# Ordered fallback chain for the Utility model (summarization, naming,
|
||||
|
||||
+2
-2
@@ -91,7 +91,7 @@ async function _createDirectChatFromPreferredModel() {
|
||||
if (!sessionModule) return false;
|
||||
|
||||
const pending = sessionModule.getPendingChat && sessionModule.getPendingChat();
|
||||
if (pending && pending.url && pending.modelId) {
|
||||
if (pending && pending.url && pending.modelId && pending.endpointId) {
|
||||
sessionModule.createDirectChat(pending.url, pending.modelId, pending.endpointId);
|
||||
return true;
|
||||
}
|
||||
@@ -99,7 +99,7 @@ async function _createDirectChatFromPreferredModel() {
|
||||
const sessions = sessionModule.getSessions();
|
||||
const currentId = sessionModule.getCurrentSessionId();
|
||||
const current = sessions.find(s => s.id === currentId);
|
||||
if (current && current.endpoint_url && current.model) {
|
||||
if (current && current.endpoint_url && current.model && current.endpoint_id) {
|
||||
sessionModule.createDirectChat(current.endpoint_url, current.model, current.endpoint_id);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2065,6 +2065,16 @@
|
||||
<label class="admin-switch"><input type="checkbox" id="adm-signupToggle"><span class="admin-slider"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-card">
|
||||
<h2><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><path d="M12 15v3m-3-3h6M12 3v2m0 16v-2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M3 12h2m16 0h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/><circle cx="12" cy="12" r="3"/></svg>Model Defaults</h2>
|
||||
<div class="admin-toggle-row">
|
||||
<div>
|
||||
<div class="admin-toggle-label">Share defaults with users</div>
|
||||
<div class="admin-toggle-sub">When on, users without a personal default inherit the global default model (only if those models are allowed for them).</div>
|
||||
</div>
|
||||
<label class="admin-switch"><input type="checkbox" id="adm-shareDefaultsToggle"><span class="admin-slider"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-card">
|
||||
<h2><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>Users</h2>
|
||||
<div id="adm-userList"><div class="admin-empty">Loading...</div></div>
|
||||
|
||||
+23
-1
@@ -343,6 +343,28 @@ function initSignupToggle() {
|
||||
});
|
||||
}
|
||||
|
||||
function initShareDefaultsToggle() {
|
||||
const toggle = el('adm-shareDefaultsToggle');
|
||||
fetch('/api/auth/settings', { credentials: 'same-origin' })
|
||||
.then(r => r.json())
|
||||
.then(d => { toggle.checked = !!d.share_defaults_with_users; })
|
||||
.catch(e => console.warn('Settings fetch failed:', e));
|
||||
toggle.addEventListener('change', async () => {
|
||||
try {
|
||||
const res = await fetch('/api/auth/settings', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ share_defaults_with_users: toggle.checked }),
|
||||
});
|
||||
const data = await res.json();
|
||||
toggle.checked = !!data.share_defaults_with_users;
|
||||
} catch (e) {
|
||||
toggle.checked = !toggle.checked;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initAddUser() {
|
||||
fetch('/api/auth/policy', { credentials: 'same-origin' })
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
@@ -2986,7 +3008,7 @@ function initLogsView() {
|
||||
function initAll() {
|
||||
modalEl = el('settings-modal');
|
||||
const inits = [
|
||||
initSignupToggle, initAddUser, initEndpointForm, initMcpForm,
|
||||
initSignupToggle, initShareDefaultsToggle, initAddUser, initEndpointForm, initMcpForm,
|
||||
initCalDAV, initBackup, initDangerZone, initTokenForm, initLogsView,
|
||||
() => settingsModule.initIntegrations()
|
||||
];
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
"""Tests for share_defaults_with_users setting"""
|
||||
import pytest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from tests.helpers.import_state import preserve_import_state
|
||||
from tests.helpers.db_stubs import make_core_db_stub
|
||||
|
||||
with preserve_import_state("core.database", "src.database", "routes.model_routes", "routes.prefs_routes"):
|
||||
import routes.model_routes as model_routes
|
||||
import routes.prefs_routes as prefs_routes
|
||||
import src.auth_helpers as auth_helpers
|
||||
|
||||
|
||||
### Helper Classes
|
||||
|
||||
class _FakeEndpoint:
|
||||
"""Minimal fake endpoint for testing"""
|
||||
def __init__(self, id, base_url, is_enabled=True, owner=None):
|
||||
self.id = id
|
||||
self.base_url = base_url
|
||||
self.is_enabled = is_enabled
|
||||
self.owner = owner
|
||||
self.cached_models = None
|
||||
self.hidden_models = None
|
||||
self.pinned_models = None
|
||||
|
||||
|
||||
class _FakeQuery:
|
||||
"""Fake query object for testing"""
|
||||
def __init__(self, endpoints, user=None, include_shared=True):
|
||||
self._endpoints = endpoints
|
||||
self._user = user
|
||||
self._include_shared = include_shared
|
||||
|
||||
def filter(self, *conditions):
|
||||
for cond in conditions:
|
||||
cond_str = str(cond)
|
||||
print(f"Filter condition: {cond_str}")
|
||||
if 'owner' in cond_str and 'IS NULL' not in cond_str:
|
||||
self._include_shared = False
|
||||
return self
|
||||
|
||||
def first(self):
|
||||
"""Return first endpoint respecting owner filter"""
|
||||
if not self._endpoints:
|
||||
return None
|
||||
|
||||
if self._user:
|
||||
for ep in self._endpoints:
|
||||
ep_owner = getattr(ep, 'owner', None)
|
||||
if ep_owner == self._user:
|
||||
return ep
|
||||
if self._include_shared and ep_owner is None:
|
||||
return ep
|
||||
return None
|
||||
return self._endpoints[0]
|
||||
|
||||
|
||||
def _make_db_session(endpoints, user=None):
|
||||
"""Create a fake DB session that returns our fake query"""
|
||||
fake_session = MagicMock()
|
||||
fake_query = _FakeQuery(endpoints, user)
|
||||
fake_session.query.return_value = fake_query
|
||||
return fake_session
|
||||
|
||||
|
||||
def _get_default_chat_route(router):
|
||||
"""Extract the /api/default-chat GET route from the router"""
|
||||
for route in router.routes:
|
||||
if getattr(route, "path", "") == "/api/default-chat" and "GET" in getattr(route, "methods", set()):
|
||||
return route.endpoint
|
||||
raise AssertionError("GET /api/default-chat route not found")
|
||||
|
||||
|
||||
def _make_request(user=None, auth_manager=None):
|
||||
"""Create a fake request for testing"""
|
||||
return SimpleNamespace(
|
||||
state=SimpleNamespace(current_user=user),
|
||||
app=SimpleNamespace(state=SimpleNamespace(auth_manager=auth_manager)),
|
||||
client=SimpleNamespace(host="127.0.0.1"),
|
||||
)
|
||||
|
||||
### Shared test logic
|
||||
def _run_get_default_chat_test(monkeypatch, share_defaults_enabled, second_endpoint_only=False):
|
||||
"""Helper function that runs get_default_chat with the given share_defaults_with_users setting."""
|
||||
|
||||
global_settings = {
|
||||
"default_endpoint_id": "global-ep-123",
|
||||
"default_model": "qwen-3.6",
|
||||
"default_model_fallbacks": [
|
||||
{"endpoint_id": "fallback-ep", "model": "fallback-model"}
|
||||
],
|
||||
"share_defaults_with_users": share_defaults_enabled
|
||||
}
|
||||
|
||||
monkeypatch.setattr(model_routes, "_load_settings", lambda: global_settings)
|
||||
monkeypatch.setattr(prefs_routes, "_load_for_user", lambda user: {})
|
||||
|
||||
fake_auth_manager = MagicMock()
|
||||
fake_auth_manager.is_admin = lambda user: False
|
||||
|
||||
endpoints = [
|
||||
_FakeEndpoint(
|
||||
id="global-ep-123",
|
||||
base_url="http://global-endpoint:8000/v1",
|
||||
is_enabled=True
|
||||
),
|
||||
_FakeEndpoint(
|
||||
id="fallback-ep",
|
||||
base_url="http://fallback-endpoint:8000/v1",
|
||||
is_enabled=True
|
||||
)
|
||||
]
|
||||
|
||||
# When testing fallback scenario, removes the primary endpoint
|
||||
if second_endpoint_only:
|
||||
endpoints = [endpoints[1]]
|
||||
|
||||
fake_db = _make_db_session(endpoints, user="regular_user")
|
||||
monkeypatch.setattr(model_routes, "SessionLocal", lambda: fake_db)
|
||||
monkeypatch.setattr(model_routes, "_normalize_base", lambda url: url)
|
||||
monkeypatch.setattr(model_routes, "build_chat_url", lambda base: f"{base}/chat")
|
||||
|
||||
router = model_routes.setup_model_routes(model_discovery=None)
|
||||
get_default_chat = _get_default_chat_route(router)
|
||||
fake_request = _make_request(user="regular_user", auth_manager=fake_auth_manager)
|
||||
|
||||
result = get_default_chat(fake_request)
|
||||
|
||||
return result
|
||||
|
||||
### Test Functions
|
||||
|
||||
def test_get_default_chat_user_no_prefs_share_disabled_resolves_nothing(monkeypatch):
|
||||
"""
|
||||
Non-admin user without personal preferences should resolve to empty
|
||||
ep_id, model, and fallbacks when share_defaults_with_users is disabled.
|
||||
"""
|
||||
|
||||
test_data = _run_get_default_chat_test(monkeypatch, share_defaults_enabled=False)
|
||||
|
||||
assert test_data["endpoint_id"] == "", "Should get empty endpoint_id"
|
||||
assert test_data["model"] == "", "Should get empty model"
|
||||
|
||||
|
||||
def test_get_default_chat_user_no_prefs_share_enabled_resolves_global_defaults_fallbacks(monkeypatch):
|
||||
"""
|
||||
Non-admin user without personal preferences should resolve to global
|
||||
defaults for ep_id, model, and fallbacks when share_defaults_with_users is enabled.
|
||||
"""
|
||||
|
||||
test_data = _run_get_default_chat_test(monkeypatch, share_defaults_enabled=True)
|
||||
|
||||
assert test_data["model"] == "qwen-3.6", \
|
||||
"model should be resolved from global default_model"
|
||||
|
||||
assert test_data["endpoint_id"] == "global-ep-123", \
|
||||
"Should get global endpoint_id"
|
||||
|
||||
def test_get_default_chat_user_no_prefs_share_enabled_resolves_global_defaults(monkeypatch):
|
||||
"""
|
||||
Non-admin user without personal preferences should resolve to global
|
||||
defaults for ep_id, model, and fallbacks when share_defaults_with_users is enabled.
|
||||
"""
|
||||
|
||||
test_data = _run_get_default_chat_test(monkeypatch, share_defaults_enabled=True, second_endpoint_only=True)
|
||||
|
||||
assert test_data["model"] == "qwen-3.6", \
|
||||
"model should be resolved from global default_model"
|
||||
|
||||
assert test_data["endpoint_id"] == "fallback-ep", \
|
||||
"Should get global endpoint_id"
|
||||
Reference in New Issue
Block a user