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:
Teknium 2026-04-13 23:09:39 -07:00 committed by GitHub
parent 35424f8fc1
commit 38ad158b6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 90 additions and 1 deletions

View file

@ -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)

View file

@ -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:

View file

@ -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"]