fix(#4728): clear stale custom:* base_url + default wins without loaded session (gate findings)

Two gate-found CORE/SILENT edges fixed on-branch:
- api/config.py: switching the main default to a different custom:* provider kept
  the OLD provider's base_url, routing New Chat to the wrong endpoint. The
  base_url-clear on provider-change now covers custom:* too (was exempted).
- static/sessions.js: newSession() skipped the configured default when S.session
  was null but the picker held stale state (e.g. after deleting the last session),
  inheriting the stale model. Default now wins over legacy picker regardless of
  loaded-session state (explicit empty-composer override still takes priority).
- Updated 2 stale source-grep precedence assertions + widened a brittle 6000-char
  newSession() slice window to 7000 (assignment block grew; intent unchanged).
- 2 new non-vacuous regression tests (custom base_url clear; precedence).

Co-authored-by: Rod Boev <rod.boev@gmail.com>
This commit is contained in:
nesquena-hermesandRod Boev committed 2026-06-24 20:17:29 +00:00
1 parent 897322eac3
commit 57a78e9284
5 files changed
+57 -8

No files matched your search

+6 -1
View File
@@ -3605,7 +3605,12 @@ def set_hermes_default_model(model_id: str, provider: str | None = None, advance
elif persisted_provider != previous_provider:
if persisted_provider == "openai":
model_cfg["base_url"] = "https://api.openai.com/v1"
elif not persisted_provider.startswith("custom:"):
else:
# Provider changed and we have no resolved URL for the new one.
# Drop the previous provider's base_url so New Chat doesn't route
# to the old endpoint — this MUST also cover custom:* providers
# (a different custom provider has a different URL); leaving the
# stale base_url sent requests to the wrong host (#4728).
model_cfg.pop("base_url", None)
_apply_advanced_model_options(model_cfg, advanced)
+3 -1
View File
@@ -886,7 +886,9 @@ async function newSession(flash, options={}){
if(!hasLoadedSession&&explicitModelOverride&&explicitModelOverride.model){
newModelState=explicitModelOverride;
consumedExplicitModelOverride=true;
}else if(hasLoadedSession&&window._defaultModel){
}else if(window._defaultModel){
// Configured default wins over stale picker/persisted state even with no
// loaded session (deleting the last session left S.session null + stale picker) (#4728).
newModelState={model:window._defaultModel,model_provider:null};
usingConfiguredDefault=true;
}else if(modelSelForNew&&modelSelForNew.value&&typeof _modelStateForSelect==='function'){
@@ -214,7 +214,7 @@ def _provider_assignment_in_new_session() -> str:
src = _read("static/sessions.js")
idx = src.find("async function newSession(flash, options={}){")
assert idx != -1, "newSession() must be defined in static/sessions.js"
body = src[idx : idx + 6000]
body = src[idx : idx + 7000]
guard_start = body.find("const _bareModel")
assert guard_start != -1, (
"newSession() must declare a 'const _bareModel' guard for the "
@@ -98,7 +98,7 @@ def test_new_session_posts_picker_model_before_server_default():
fn = _new_session_function()
assert "reqBody.model=newModelState.model" in fn
assert "explicitModelOverride" in fn
assert "hasLoadedSession&&window._defaultModel" in fn
assert "}else if(window._defaultModel){" in fn
assert "modelSelForNew&&modelSelForNew.value&&typeof _modelStateForSelect==='function'" in fn
provider_assignment = fn[fn.index("reqBody.model_provider="):].split(";", 1)[0]
assert "newModelState.model_provider" in provider_assignment
@@ -106,10 +106,10 @@ def test_new_session_posts_picker_model_before_server_default():
assert "window._activeProvider" in fn
assert "S.session&&S.session.model_provider" in fn
pos_override = fn.index("explicitModelOverride")
pos_default = fn.index("hasLoadedSession&&window._defaultModel")
pos_default = fn.index("}else if(window._defaultModel){")
pos_legacy = fn.index("modelSelForNew&&modelSelForNew.value&&typeof _modelStateForSelect==='function'")
assert pos_override < pos_default < pos_legacy, (
"newSession() must prefer the empty-composer override first, then the configured default for loaded-session flows, then legacy picker state"
"newSession() must prefer the empty-composer override first, then the configured default, then legacy picker state"
)
assert "_familyMismatch" in fn
assert "_readPersistedModelState" in fn
@@ -140,8 +140,8 @@ def test_new_chat_prefers_explicit_empty_composer_override_before_configured_def
assert "usingConfiguredDefault" in fn
assert "_clearEmptyComposerModelOverride" in fn
assert "newModelState={model:window._defaultModel,model_provider:null};" in fn
assert fn.index("explicitModelOverride") < fn.index("hasLoadedSession&&window._defaultModel") < fn.index("_modelStateForSelect"), (
"newSession() must prefer the empty-composer override first, then the configured default for loaded-session flows, then legacy picker state"
assert fn.index("explicitModelOverride") < fn.index("}else if(window._defaultModel){") < fn.index("_modelStateForSelect"), (
"newSession() must prefer the empty-composer override first, then the configured default, then legacy picker state"
)
+42
View File
@@ -380,6 +380,48 @@ def test_default_model_save_persists_codex_provider_for_qualified_model(tmp_path
assert saved["model"].get("base_url") != "https://openrouter.ai/api/v1"
def test_default_model_save_clears_stale_custom_base_url_on_provider_change(tmp_path, monkeypatch):
"""Switching the main default from one custom provider to another must drop
the previous provider's base_url so New Chat doesn't route to the old
endpoint (#4728). Previously custom:* was exempted from the base_url clear,
so the stale URL lingered."""
import yaml
import api.config as config
config_file = tmp_path / "config.yaml"
config_file.write_text(
"model:\n"
" provider: custom:old-local\n"
" default: old-model\n"
" base_url: http://old.local/v1\n",
encoding="utf-8",
)
old_cfg = dict(config.cfg)
old_mtime = config._cfg_mtime
monkeypatch.setattr(config, "_get_config_path", lambda: config_file)
config.cfg["model"] = {
"provider": "custom:old-local",
"default": "old-model",
"base_url": "http://old.local/v1",
}
config._cfg_mtime = config_file.stat().st_mtime
try:
result = config.set_hermes_default_model("new-model", provider="custom:new-local")
saved = yaml.safe_load(config_file.read_text(encoding="utf-8"))
finally:
config.cfg.clear()
config.cfg.update(old_cfg)
config._cfg_mtime = old_mtime
config.invalidate_models_cache()
assert result["ok"] is True
assert saved["model"]["provider"] == "custom:new-local"
# The stale base_url from the OLD custom provider must not survive.
assert saved["model"].get("base_url") != "http://old.local/v1", (
f"stale custom base_url leaked: {saved['model'].get('base_url')!r}"
)
def test_active_codex_at_provider_session_model_preserved(monkeypatch):
"""@openai-codex:gpt-5.5 session selections must keep their provider hint."""
import api.routes as routes