mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix: auto-correct close model name matches in /model validation (#9424)
* feat(skills): add fitness-nutrition skill to optional-skills Cherry-picked from PR #9177 by @haileymarshall. Adds a fitness and nutrition skill for gym-goers and health-conscious users: - Exercise search via wger API (690+ exercises, free, no auth) - Nutrition lookup via USDA FoodData Central (380K+ foods, DEMO_KEY fallback) - Offline body composition calculators (BMI, TDEE, 1RM, macros, body fat %) - Pure stdlib Python, no pip dependencies Changes from original PR: - Moved from skills/ to optional-skills/health/ (correct location) - Fixed BMR formula in FORMULAS.md (removed confusing -5+10, now just +5) - Fixed author attribution to match PR submitter - Marked USDA_API_KEY as optional (DEMO_KEY works without signup) Also adds optional env var support to the skill readiness checker: - New 'optional: true' field in required_environment_variables entries - Optional vars are preserved in metadata but don't block skill readiness - Optional vars skip the CLI capture prompt flow - Skills with only optional missing vars show as 'available' not 'setup_needed' * fix: auto-correct close model name matches in /model validation When a user types a model name with a minor typo (e.g. gpt5.3-codex instead of gpt-5.3-codex), the validation now auto-corrects to the closest match instead of accepting the wrong name with a warning. Uses difflib get_close_matches with cutoff=0.9 to avoid false corrections (e.g. gpt-5.3 should not silently become gpt-5.4). Applied consistently across all three validation paths: codex provider, custom endpoints, and generic API-probed providers. The validate_requested_model() return dict gains an optional corrected_model key that switch_model() applies before building the result. Reported by Discord user — /model gpt5.3-codex was accepted with a warning but would fail at the API level. --------- Co-authored-by: haileymarshall <haileymarshall@users.noreply.github.com>
This commit is contained in:
parent
35424f8fc1
commit
38ad158b6b
3 changed files with 90 additions and 1 deletions
|
|
@ -705,6 +705,10 @@ def switch_model(
|
|||
error_message=msg,
|
||||
)
|
||||
|
||||
# Apply auto-correction if validation found a closer match
|
||||
if validation.get("corrected_model"):
|
||||
new_model = validation["corrected_model"]
|
||||
|
||||
# --- OpenCode api_mode override ---
|
||||
if target_provider in {"opencode-zen", "opencode-go", "opencode", "opencode-go"}:
|
||||
api_mode = opencode_model_api_mode(target_provider, new_model)
|
||||
|
|
|
|||
|
|
@ -1820,6 +1820,17 @@ def validate_requested_model(
|
|||
"message": None,
|
||||
}
|
||||
|
||||
# Auto-correct if the top match is very similar (e.g. typo)
|
||||
auto = get_close_matches(requested_for_lookup, api_models, n=1, cutoff=0.9)
|
||||
if auto:
|
||||
return {
|
||||
"accepted": True,
|
||||
"persist": True,
|
||||
"recognized": True,
|
||||
"corrected_model": auto[0],
|
||||
"message": f"Auto-corrected `{requested}` → `{auto[0]}`",
|
||||
}
|
||||
|
||||
suggestions = get_close_matches(requested, api_models, n=3, cutoff=0.5)
|
||||
suggestion_text = ""
|
||||
if suggestions:
|
||||
|
|
@ -1871,6 +1882,16 @@ def validate_requested_model(
|
|||
"recognized": True,
|
||||
"message": None,
|
||||
}
|
||||
# Auto-correct if the top match is very similar (e.g. typo)
|
||||
auto = get_close_matches(requested_for_lookup, codex_models, n=1, cutoff=0.9)
|
||||
if auto:
|
||||
return {
|
||||
"accepted": True,
|
||||
"persist": True,
|
||||
"recognized": True,
|
||||
"corrected_model": auto[0],
|
||||
"message": f"Auto-corrected `{requested}` → `{auto[0]}`",
|
||||
}
|
||||
suggestions = get_close_matches(requested_for_lookup, codex_models, n=3, cutoff=0.5)
|
||||
suggestion_text = ""
|
||||
if suggestions:
|
||||
|
|
@ -1903,6 +1924,18 @@ def validate_requested_model(
|
|||
# the user may have access to models not shown in the public
|
||||
# listing (e.g. Z.AI Pro/Max plans can use glm-5 on coding
|
||||
# endpoints even though it's not in /models). Warn but allow.
|
||||
|
||||
# Auto-correct if the top match is very similar (e.g. typo)
|
||||
auto = get_close_matches(requested_for_lookup, api_models, n=1, cutoff=0.9)
|
||||
if auto:
|
||||
return {
|
||||
"accepted": True,
|
||||
"persist": True,
|
||||
"recognized": True,
|
||||
"corrected_model": auto[0],
|
||||
"message": f"Auto-corrected `{requested}` → `{auto[0]}`",
|
||||
}
|
||||
|
||||
suggestions = get_close_matches(requested, api_models, n=3, cutoff=0.5)
|
||||
suggestion_text = ""
|
||||
if suggestions:
|
||||
|
|
|
|||
|
|
@ -436,7 +436,22 @@ class TestValidateApiNotFound:
|
|||
def test_warning_includes_suggestions(self):
|
||||
result = _validate("anthropic/claude-opus-4.5")
|
||||
assert result["accepted"] is True
|
||||
assert "Similar models" in result["message"]
|
||||
# Close match auto-corrects; less similar inputs show suggestions
|
||||
assert "Auto-corrected" in result["message"] or "Similar models" in result["message"]
|
||||
|
||||
def test_auto_correction_returns_corrected_model(self):
|
||||
"""When a very close match exists, validate returns corrected_model."""
|
||||
result = _validate("anthropic/claude-opus-4.5")
|
||||
assert result["accepted"] is True
|
||||
assert result.get("corrected_model") == "anthropic/claude-opus-4.6"
|
||||
assert result["recognized"] is True
|
||||
|
||||
def test_dissimilar_model_shows_suggestions_not_autocorrect(self):
|
||||
"""Models too different for auto-correction still get suggestions."""
|
||||
result = _validate("anthropic/claude-nonexistent")
|
||||
assert result["accepted"] is True
|
||||
assert result.get("corrected_model") is None
|
||||
assert "not found" in result["message"]
|
||||
|
||||
|
||||
# -- validate — API unreachable — accept and persist everything ----------------
|
||||
|
|
@ -486,3 +501,40 @@ class TestValidateApiFallback:
|
|||
assert result["persist"] is True
|
||||
assert "http://localhost:8000/v1/models" in result["message"]
|
||||
assert "http://localhost:8000/v1" in result["message"]
|
||||
|
||||
|
||||
# -- validate — Codex auto-correction ------------------------------------------
|
||||
|
||||
class TestValidateCodexAutoCorrection:
|
||||
"""Auto-correction for typos on openai-codex provider."""
|
||||
|
||||
def test_missing_dash_auto_corrects(self):
|
||||
"""gpt5.3-codex (missing dash) auto-corrects to gpt-5.3-codex."""
|
||||
codex_models = ["gpt-5.4-mini", "gpt-5.4", "gpt-5.3-codex",
|
||||
"gpt-5.2-codex", "gpt-5.1-codex-max"]
|
||||
with patch("hermes_cli.models.provider_model_ids", return_value=codex_models):
|
||||
result = validate_requested_model("gpt5.3-codex", "openai-codex")
|
||||
assert result["accepted"] is True
|
||||
assert result["recognized"] is True
|
||||
assert result["corrected_model"] == "gpt-5.3-codex"
|
||||
assert "Auto-corrected" in result["message"]
|
||||
|
||||
def test_exact_match_no_correction(self):
|
||||
"""Exact model name does not trigger auto-correction."""
|
||||
codex_models = ["gpt-5.4-mini", "gpt-5.4", "gpt-5.3-codex"]
|
||||
with patch("hermes_cli.models.provider_model_ids", return_value=codex_models):
|
||||
result = validate_requested_model("gpt-5.3-codex", "openai-codex")
|
||||
assert result["accepted"] is True
|
||||
assert result["recognized"] is True
|
||||
assert result.get("corrected_model") is None
|
||||
assert result["message"] is None
|
||||
|
||||
def test_very_different_name_falls_to_suggestions(self):
|
||||
"""Names too different for auto-correction get the suggestion list."""
|
||||
codex_models = ["gpt-5.4-mini", "gpt-5.4", "gpt-5.3-codex"]
|
||||
with patch("hermes_cli.models.provider_model_ids", return_value=codex_models):
|
||||
result = validate_requested_model("totally-wrong", "openai-codex")
|
||||
assert result["accepted"] is True
|
||||
assert result["recognized"] is False
|
||||
assert result.get("corrected_model") is None
|
||||
assert "not found" in result["message"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue