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:
1 parent
897322eac3
commit
57a78e9284
5 files changed
+57
-8
No files matched your search
+6
-1
@@ -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
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user