fix(webui): harden reasoning chip provider coverage

This commit is contained in:
Roberto Villegas
2026-05-24 18:30:16 -06:00
parent d1471fbed7
commit 4c4922a0d5
6 changed files with 55 additions and 18 deletions

View File

@@ -1031,6 +1031,7 @@ $('modelSelect').onchange=async()=>{
else try{localStorage.setItem('hermes-webui-model',modelState.model)}catch{}
if(!S.session){
if(typeof syncModelChip==='function') syncModelChip();
if(typeof syncReasoningChip==='function') syncReasoningChip();
return;
}
if(typeof _rememberPendingSessionModel==='function') _rememberPendingSessionModel(S.session.session_id,modelState.model,modelState.model_provider);

View File

@@ -967,11 +967,14 @@ function _ensureModelOptionInDropdown(modelId, sel, preferredProviderId){
if(!modelId||!sel) return null;
const applied=_applyModelToDropdown(modelId,sel,preferredProviderId);
if(applied) return applied;
const value=modelId;
const opt=document.createElement('option');
opt.value=modelId;
opt.textContent=typeof getModelLabel==='function'?getModelLabel(modelId):modelId;
opt.dataset.custom='1';
const provider=preferredProviderId||_providerFromModelValue(modelId)||'';
const badge=(window._configuredModelBadges||{})[value];
if(badge&&badge.provider) opt.dataset.provider=badge.provider;
const provider=preferredProviderId||(badge&&badge.provider)||_providerFromModelValue(modelId)||'';
if(provider) opt.dataset.provider=provider;
sel.appendChild(opt);
sel.value=modelId;
@@ -1554,7 +1557,8 @@ function renderModelDropdown(){
_filterModels('');
}
async function selectModelFromDropdown(value, preferredProviderId){
async function selectModelFromDropdown(value){
const preferredProviderId=arguments[1];
const sel=$('modelSelect');
if(!sel) { closeModelDropdown(); return; }
const provider=String(preferredProviderId||'').trim()||null;
@@ -1673,7 +1677,8 @@ function _applyReasoningOptions(supportedEfforts){
});
}
function _applyReasoningChip(eff, meta){
function _applyReasoningChip(eff){
const meta=arguments[1]||null;
const effort=_normalizeReasoningEffort(eff);
_currentReasoningEffort=effort;
if(meta&&Array.isArray(meta.supported_efforts)){
@@ -1685,8 +1690,11 @@ function _applyReasoningChip(eff, meta){
const mobileLabel=$('composerMobileReasoningLabel');
const mobileAction=$('composerMobileReasoningAction');
if(!wrap||!label) return;
const supports=Array.isArray(_currentReasoningEffortsSupported)
?_currentReasoningEffortsSupported.length>0
const supportedEfforts=(typeof _currentReasoningEffortsSupported==='undefined')
?null
:_currentReasoningEffortsSupported;
const supports=Array.isArray(supportedEfforts)
?supportedEfforts.length>0
:true;
if(!supports){
wrap.style.display='none';
@@ -1695,7 +1703,7 @@ function _applyReasoningChip(eff, meta){
}
wrap.style.display='';
if(mobileAction) mobileAction.style.display='';
_applyReasoningOptions(_currentReasoningEffortsSupported);
if(typeof _applyReasoningOptions==='function') _applyReasoningOptions(supportedEfforts);
const text=_formatReasoningEffortLabel(effort);
label.textContent=text;
if(mobileLabel) mobileLabel.textContent=text;

View File

@@ -78,7 +78,7 @@ def test_syncReasoningChip_called_on_session_load():
def test_syncReasoningChip_called_on_model_change():
"""Model picker changes must refresh reasoning chip after session model updates."""
"""Model picker changes must refresh reasoning chip with or without a session."""
with open("static/boot.js") as f:
boot_src = f.read()
marker = "$('modelSelect').onchange=async()=>{"
@@ -86,8 +86,11 @@ def test_syncReasoningChip_called_on_model_change():
tail = boot_src[start:]
assert "syncReasoningChip()" in tail, \
"syncReasoningChip() must be called when modelSelect changes"
no_session = tail[tail.index("if(!S.session){"):tail.index("if(typeof _rememberPendingSessionModel")]
assert "syncReasoningChip()" in no_session, \
"syncReasoningChip() must also run for pre-session picker changes"
model_assign = tail.index("S.session.model=modelState.model")
sync_call = tail.index("syncReasoningChip()")
sync_call = tail.index("syncReasoningChip()", model_assign)
assert model_assign < sync_call, \
"syncReasoningChip() must run after S.session.model is updated"

View File

@@ -11,21 +11,19 @@ def _read_ui() -> str:
def test_select_model_custom_option_uses_friendly_label_helper():
src = _read_ui()
start = src.find("async function selectModelFromDropdown(value)")
assert start != -1, "selectModelFromDropdown() not found"
end = src.find("\nfunction toggleModelDropdown()", start)
if end == -1:
end = src.find("\nasync function toggleModelDropdown()", start)
assert end != -1, "toggleModelDropdown() boundary not found"
start = src.find("function _ensureModelOptionInDropdown")
assert start != -1, "_ensureModelOptionInDropdown() not found"
end = src.find("\nfunction _modelStateFromAppliedDropdown", start)
assert end != -1, "_modelStateFromAppliedDropdown() boundary not found"
body = src[start:end]
assert "opt.textContent=getModelLabel(value);" in body, (
"Temporary model options should use getModelLabel(value) so the chip shows a "
assert "getModelLabel(modelId)" in body, (
"Temporary model options should use getModelLabel(modelId) so the chip shows a "
"friendly label instead of a raw slug when the value is not already in the "
"native <select> options."
)
assert "opt.textContent=value.split('/').pop()||value;" not in body, (
"Raw slug fallback in selectModelFromDropdown() regresses the model chip for "
"Raw slug fallback in temporary model options regresses the model chip for "
"Ollama-tag style model IDs."
)

View File

@@ -1136,7 +1136,9 @@ class TestModelSwitchToast:
# Find the onchange block
idx = src.find("modelSelect').onchange")
assert idx != -1, "modelSelect.onchange not found in boot.js"
block = src[idx:idx + 1100]
end = src.find("$('msg').addEventListener", idx)
assert end != -1, "modelSelect.onchange block terminator not found in boot.js"
block = src[idx:end]
assert "model_scope_toast" in block, (
"modelSelect.onchange must show that the selected model applies to this conversation"
)

View File

@@ -28,6 +28,31 @@ def test_openai_codex_prefixed_gpt5_supports_reasoning_effort_levels():
assert "high" in efforts
def test_github_copilot_gpt5_supports_reasoning_effort_levels():
efforts = cfg.resolve_model_reasoning_efforts(
"gpt-5.5",
provider_id="github-copilot",
)
assert "medium" in efforts
assert "high" in efforts
def test_openrouter_anthropic_models_keep_reasoning_effort_levels():
efforts = cfg.resolve_model_reasoning_efforts(
"anthropic/claude-sonnet-4.5",
provider_id="openrouter",
)
assert "medium" in efforts
assert "high" in efforts
def test_non_reasoning_http_models_hide_reasoning_effort_levels():
assert cfg.resolve_model_reasoning_efforts(
"meta-llama/llama-3.1-8b-instruct",
provider_id="openrouter",
) == []
def test_get_reasoning_status_includes_supported_efforts(monkeypatch):
monkeypatch.setattr(
cfg,