diff --git a/api/models.py b/api/models.py index 233af200..8ab527eb 100644 --- a/api/models.py +++ b/api/models.py @@ -1340,6 +1340,27 @@ def get_session(sid, metadata_only=False): return s raise KeyError(sid) +def _profile_default_model_state(profile=None): + """Return the default model/provider configured for *profile*.""" + default_model = "" + default_provider = None + try: + from api.profiles import get_hermes_home_for_profile + config_path = Path(get_hermes_home_for_profile(profile)) / "config.yaml" + config_data = _cfg._load_yaml_config_file(config_path) + except Exception: + config_data = {} + + model_cfg = config_data.get("model", {}) if isinstance(config_data, dict) else {} + if isinstance(model_cfg, str): + default_model = model_cfg.strip() + elif isinstance(model_cfg, dict): + default_model = str(model_cfg.get("default") or "").strip() + default_provider = str(model_cfg.get("provider") or "").strip() or None + + return default_model or get_effective_default_model(), default_provider + + def new_session(workspace=None, model=None, profile=None, model_provider=None, project_id=None, worktree_info=None): """Create a new in-memory session. @@ -1372,13 +1393,19 @@ def new_session(workspace=None, model=None, profile=None, model_provider=None, p profile = get_active_profile_name() except ImportError: profile = None - effective_model = model or get_effective_default_model() + if model: + effective_model = model + effective_model_provider = model_provider + else: + effective_model, effective_model_provider = _profile_default_model_state(profile) + if model_provider: + effective_model_provider = model_provider wt = worktree_info if isinstance(worktree_info, dict) else None workspace_path = (wt.get('path') if wt and wt.get('path') else workspace) if wt else workspace s = Session( workspace=workspace_path or get_last_workspace(), model=effective_model, - model_provider=model_provider, + model_provider=effective_model_provider, profile=profile, project_id=project_id, worktree_path=wt.get('path') if wt else None, diff --git a/static/sessions.js b/static/sessions.js index 503b5bbe..3d61eae2 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -460,23 +460,7 @@ async function newSession(flash, options={}){ const switchWs=S._profileSwitchWorkspace; S._profileSwitchWorkspace=null; const inheritWs=switchWs||(S.session?S.session.workspace:null)||(S._profileDefaultWorkspace||null); - // Use the saved default model for new sessions (#872). The user's saved - // default_model (from Settings) takes priority over the chat-header dropdown - // value, which reflects the *previous* session's model. Fall back to the - // dropdown value only when no default_model is configured. - const modelSel=$('modelSelect'); - const selectedDefaultModel=window._defaultModel||(modelSel&&modelSel.value)||''; - let defaultApplied=false; - if(window._defaultModel&&modelSel&&typeof _applyModelToDropdown==='function'){ - defaultApplied=!!_applyModelToDropdown(window._defaultModel,modelSel,window._activeProvider||null); - } - const canQualify=!window._defaultModel||defaultApplied||(modelSel&&modelSel.value===selectedDefaultModel); - const newModelState=(canQualify&&typeof _modelStateForSelect==='function') - ? _modelStateForSelect(modelSel,selectedDefaultModel) - : {model:selectedDefaultModel,model_provider:null}; const reqBody={ - model:newModelState.model, - model_provider:newModelState.model_provider||null, workspace:inheritWs, profile:S.activeProfile||'default', }; diff --git a/tests/test_issue798.py b/tests/test_issue798.py index 37889688..5f5fd6a1 100644 --- a/tests/test_issue798.py +++ b/tests/test_issue798.py @@ -266,6 +266,30 @@ def test_sessions_js_sends_profile_in_new_session_post(): ) +def test_new_session_uses_explicit_profile_default_model_and_provider(tmp_path, monkeypatch): + """New chats must inherit the selected profile's config default.""" + import api.profiles as p + import api.models as m + + monkeypatch.setattr(p, "_DEFAULT_HERMES_HOME", tmp_path) + profile_home = tmp_path / "profiles" / "pepper" + profile_home.mkdir(parents=True) + (profile_home / "config.yaml").write_text( + "model:\n default: pepper-profile-model\n provider: pepper-provider\n", + encoding="utf-8", + ) + + with patch.object(m.Session, 'save', return_value=None): + s = m.new_session(workspace=str(tmp_path), profile="pepper") + try: + assert s.model == "pepper-profile-model" + assert s.model_provider == "pepper-provider" + assert s.profile == "pepper" + finally: + with m.LOCK: + m.SESSIONS.pop(s.session_id, None) + + def test_get_hermes_home_for_profile_rejects_path_traversal(): """R19j: get_hermes_home_for_profile() must reject names that don't match _PROFILE_ID_RE (e.g. path traversal like '../../etc') and return the base diff --git a/tests/test_provider_mismatch.py b/tests/test_provider_mismatch.py index d5ac11f6..dbd2c019 100644 --- a/tests/test_provider_mismatch.py +++ b/tests/test_provider_mismatch.py @@ -1171,10 +1171,13 @@ class TestFrontendModelProviderState: assert "_modelStateForSelect" in src assert "model_provider:modelState.model_provider||null" in src - def test_new_session_sends_model_provider(self): + def test_new_session_lets_profile_config_choose_default_model_provider(self): src = _read("static/sessions.js") - assert "_modelStateForSelect(modelSel,selectedDefaultModel)" in src - assert "model_provider:newModelState.model_provider||null" in src + start = src.index("async function newSession(") + body = src[start:src.index("const data=await api('/api/session/new'", start)] + assert "profile:S.activeProfile||'default'" in body + assert "model:newModelState.model" not in body + assert "model_provider:newModelState.model_provider||null" not in body def test_ui_has_json_model_state_storage(self): src = _read("static/ui.js")