Compare commits

...

1 Commits

Author SHA1 Message Date
Timmy Time
1899878c27 Fix #373: fallback_model blank fields no longer trigger gateway warnings
Some checks failed
Forge CI / smoke-and-build (pull_request) Failing after 1m1s
When users blank fallback_model fields or set enabled: false, the validation
and gateway now treat this as intentionally disabling fallback instead of
showing warnings.

Changes:
- hermes_cli/config.py: Skip warnings when both provider and model are blank
  or when enabled: false is set
- gateway/run.py: Return None for disabled fallback configs
- tests: Added 8 new tests for blank/disabled fallback scenarios

Behavior:
- Both fields blank: no warnings (intentional disable)
- enabled: false: no warnings (explicit disable)
- One field blank: warning shown (likely misconfiguration)
- Valid config: no warnings

Fixes #373
2026-04-13 20:19:21 -04:00
3 changed files with 120 additions and 13 deletions

View File

@@ -1026,6 +1026,16 @@ class GatewayRunner:
cfg = _y.safe_load(_f) or {}
fb = cfg.get("fallback_providers") or cfg.get("fallback_model") or None
if fb:
# Treat empty dict / disabled fallback as "not configured"
if isinstance(fb, dict):
_enabled = fb.get("enabled")
if _enabled is False or (
isinstance(_enabled, str)
and _enabled.strip().lower() in ("false", "0", "no", "off")
):
return None
if not fb.get("provider") and not fb.get("model"):
return None
return fb
except Exception:
pass

View File

@@ -1421,6 +1421,7 @@ def validate_config_structure(config: Optional[Dict[str, Any]] = None) -> List["
))
# ── fallback_model must be a top-level dict with provider + model ────
# Blank or explicitly disabled fallback is intentional — skip validation.
fb = config.get("fallback_model")
if fb is not None:
if not isinstance(fb, dict):
@@ -1430,21 +1431,40 @@ def validate_config_structure(config: Optional[Dict[str, Any]] = None) -> List["
"Change to:\n"
" fallback_model:\n"
" provider: openrouter\n"
" model: anthropic/claude-sonnet-4",
" model: anthropic/claude-sonnet-4\n"
"Or disable with:\n"
" fallback_model:\n"
" enabled: false",
))
elif fb:
if not fb.get("provider"):
issues.append(ConfigIssue(
"warning",
"fallback_model is missing 'provider' field — fallback will be disabled",
"Add: provider: openrouter (or another provider)",
))
if not fb.get("model"):
issues.append(ConfigIssue(
"warning",
"fallback_model is missing 'model' field — fallback will be disabled",
"Add: model: anthropic/claude-sonnet-4 (or another model)",
))
# Skip warnings when fallback is explicitly disabled (enabled: false)
_enabled = fb.get("enabled")
if _enabled is False or (isinstance(_enabled, str) and _enabled.strip().lower() in ("false", "0", "no", "off")):
pass # intentionally disabled — no warnings
else:
# Check if both fields are blank (intentional disable)
provider = fb.get("provider")
model = fb.get("model")
provider_blank = not provider or (isinstance(provider, str) and not provider.strip())
model_blank = not model or (isinstance(model, str) and not model.strip())
# Only warn if at least one field is set (user might be trying to configure)
# If both are blank, treat as intentionally disabled
if not provider_blank or not model_blank:
if provider_blank:
issues.append(ConfigIssue(
"warning",
"fallback_model is missing 'provider' field — fallback will be disabled",
"Add: provider: openrouter (or another provider)\n"
"Or disable with: enabled: false",
))
if model_blank:
issues.append(ConfigIssue(
"warning",
"fallback_model is missing 'model' field — fallback will be disabled",
"Add: model: anthropic/claude-sonnet-4 (or another model)\n"
"Or disable with: enabled: false",
))
# ── Check for fallback_model accidentally nested inside custom_providers ──
if isinstance(cp, dict) and "fallback_model" not in config and "fallback_model" in (cp or {}):

View File

@@ -136,6 +136,83 @@ class TestFallbackModelValidation:
fb_issues = [i for i in issues if "fallback" in i.message.lower()]
assert len(fb_issues) == 0
def test_blank_fallback_fields_no_issues(self):
"""Blank fallback_model fields (both empty) should not trigger warnings."""
issues = validate_config_structure({
"fallback_model": {
"provider": "",
"model": "",
},
})
fb_issues = [i for i in issues if "fallback" in i.message.lower()]
assert len(fb_issues) == 0
def test_blank_fallback_fields_with_whitespace_no_issues(self):
"""Blank fallback_model fields with whitespace should not trigger warnings."""
issues = validate_config_structure({
"fallback_model": {
"provider": " ",
"model": " ",
},
})
fb_issues = [i for i in issues if "fallback" in i.message.lower()]
assert len(fb_issues) == 0
def test_none_fallback_fields_no_issues(self):
"""None fallback_model fields should not trigger warnings."""
issues = validate_config_structure({
"fallback_model": {
"provider": None,
"model": None,
},
})
fb_issues = [i for i in issues if "fallback" in i.message.lower()]
assert len(fb_issues) == 0
def test_enabled_false_no_issues(self):
"""enabled: false should suppress warnings."""
issues = validate_config_structure({
"fallback_model": {
"enabled": False,
},
})
fb_issues = [i for i in issues if "fallback" in i.message.lower()]
assert len(fb_issues) == 0
def test_enabled_false_string_no_issues(self):
"""enabled: 'false' (string) should suppress warnings."""
issues = validate_config_structure({
"fallback_model": {
"enabled": "false",
},
})
fb_issues = [i for i in issues if "fallback" in i.message.lower()]
assert len(fb_issues) == 0
def test_partial_blank_fallback_warns(self):
"""Partial blank fallback (only one field blank) should warn."""
issues = validate_config_structure({
"fallback_model": {
"provider": "",
"model": "anthropic/claude-sonnet-4",
},
})
fb_issues = [i for i in issues if "fallback" in i.message.lower()]
assert len(fb_issues) == 1
assert "provider" in fb_issues[0].message
def test_valid_fallback_with_enabled_true(self):
"""Valid fallback with enabled: true should not warn."""
issues = validate_config_structure({
"fallback_model": {
"enabled": True,
"provider": "openrouter",
"model": "anthropic/claude-sonnet-4",
},
})
fb_issues = [i for i in issues if "fallback" in i.message.lower()]
assert len(fb_issues) == 0
class TestMissingModelSection:
"""Warn when custom_providers exists but model section is missing."""