From 38ad158b6bd3ac4c2e68745f4f03916ece3b2305 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 13 Apr 2026 23:09:39 -0700 Subject: [PATCH] fix: auto-correct close model name matches in /model validation (#9424) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- hermes_cli/model_switch.py | 4 ++ hermes_cli/models.py | 33 ++++++++++++++ tests/hermes_cli/test_model_validation.py | 54 ++++++++++++++++++++++- 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index c777527f2..699bde23e 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -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) diff --git a/hermes_cli/models.py b/hermes_cli/models.py index 483d4a309..852601229 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -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: diff --git a/tests/hermes_cli/test_model_validation.py b/tests/hermes_cli/test_model_validation.py index af1d89ae8..5ed6b9d54 100644 --- a/tests/hermes_cli/test_model_validation.py +++ b/tests/hermes_cli/test_model_validation.py @@ -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"]