fix: warn on provider/model mismatch, surface auth errors (#266)
* fix: warn on provider/model mismatch, surface auth errors (#266) Fixes #266 — WebUI silently ignores provider/model selection mismatch. The problem: selecting an OpenRouter (or Anthropic/OpenAI) model while Hermes is configured for a different provider (e.g. local Ollama) sends the request to the wrong endpoint, which returns a 401 Unauthorized error with no UI indication of why. Three-layer fix: 1. api/streaming.py — detect 401/auth errors explicitly Added is_auth_error detection covering '401', 'AuthenticationError', 'authentication', 'unauthorized', 'invalid api key', and the specific Ollama error string 'no cookie auth credentials'. Auth errors emit apperror with type='auth_mismatch' and a hint pointing to 'hermes model'. 2. static/ui.js — expose active_provider and warn on selection - populateModelDropdown() stores data.active_provider from /api/models as window._activeProvider (the field was already in the response but the frontend never used it) - New _checkProviderMismatch(modelId) helper: compares the selected model's slash-prefix (e.g. 'openai/' from 'openai/gpt-4o') against the active provider. Skips the check for 'openrouter' and 'custom' to avoid false positives on configs that legitimately route any model. 3. static/boot.js — warn on model dropdown change modelSelect.onchange calls _checkProviderMismatch() and shows a toast when the selected model looks incompatible with the configured provider. 4. static/messages.js — distinct UI label for auth errors apperror handler now distinguishes type='auth_mismatch' and shows 'Provider mismatch' as the error label instead of 'Error'. 5. static/i18n.js — provider_mismatch_warning and provider_mismatch_label keys added to all 5 locales (en, es, de, zh-Hans, zh-Hant). Tests: 21 new tests in tests/test_provider_mismatch.py covering all five change areas. 679/679 total pass (658 baseline + 21 new). * fix: t() call args spread + use i18n label for auth mismatch 1. ui.js: _checkProviderMismatch passed [modelId, ap] as a single array arg to t(). Since t(key, ...args) spreads, the function received the array as m and undefined as p. Fixed to pass as separate args: t('provider_mismatch_warning', modelId, ap). 2. messages.js: 'Provider mismatch' label was hardcoded instead of using t('provider_mismatch_label'). Now uses the i18n key with fallback for when t() isn't available. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
28
static/ui.js
28
static/ui.js
@@ -45,6 +45,8 @@ async function populateModelDropdown(){
|
||||
try{
|
||||
const data=await fetch(new URL('/api/models',location.origin).href,{credentials:'include'}).then(r=>r.json());
|
||||
if(!data.groups||!data.groups.length) return; // keep HTML defaults
|
||||
// Store active provider globally so the send path can warn on mismatch
|
||||
window._activeProvider=data.active_provider||null;
|
||||
// Clear existing options
|
||||
sel.innerHTML='';
|
||||
_dynamicModelLabels={};
|
||||
@@ -70,6 +72,32 @@ async function populateModelDropdown(){
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given model ID belongs to a different provider than the one
|
||||
* currently configured in Hermes. Returns a warning string if mismatched,
|
||||
* or null if the selection looks compatible.
|
||||
*
|
||||
* Provider detection is intentionally loose — we compare the model's slash
|
||||
* prefix (e.g. "openai/" from "openai/gpt-4o") against the active provider
|
||||
* name. Custom/local endpoints report active_provider='custom' or the
|
||||
* base_url hostname and we skip the check to avoid false positives.
|
||||
*/
|
||||
function _checkProviderMismatch(modelId){
|
||||
const ap=(window._activeProvider||'').toLowerCase();
|
||||
if(!ap||ap==='custom'||ap==='openrouter') return null; // can't reliably check
|
||||
const slash=modelId.indexOf('/');
|
||||
if(slash<0) return null; // bare model name, no provider prefix
|
||||
const modelProvider=modelId.substring(0,slash).toLowerCase();
|
||||
// Normalise common aliases
|
||||
const aliases={'claude':'anthropic','gpt':'openai','gemini':'google'};
|
||||
const norm=p=>aliases[p]||p;
|
||||
if(norm(modelProvider)!==norm(ap)){
|
||||
return (window.t?window.t('provider_mismatch_warning',modelId,ap):
|
||||
`"${modelId}" may not work with your configured provider (${ap}). Send anyway or run \`hermes model\` to switch.`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Scroll pinning ──────────────────────────────────────────────────────────
|
||||
// When streaming, auto-scroll only if the user hasn't manually scrolled up.
|
||||
// Once the user scrolls back to within 80px of the bottom, re-pin.
|
||||
|
||||
Reference in New Issue
Block a user