fix(webui): harden reasoning chip provider coverage
This commit is contained in:
@@ -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);
|
||||
|
||||
20
static/ui.js
20
static/ui.js
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user