From 8a365bfe990e68be6da9fa7225ca143d4fd6dad2 Mon Sep 17 00:00:00 2001 From: NetVar <89574102+NetVar1337@users.noreply.github.com> Date: Sun, 12 Jul 2026 18:33:59 +0200 Subject: [PATCH 1/4] test(llm): cover invalid model remediation --- .../tests/unit/llm/test_factory_classify_errors.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/decepticon/tests/unit/llm/test_factory_classify_errors.py b/packages/decepticon/tests/unit/llm/test_factory_classify_errors.py index 0828b5f5..658e2741 100644 --- a/packages/decepticon/tests/unit/llm/test_factory_classify_errors.py +++ b/packages/decepticon/tests/unit/llm/test_factory_classify_errors.py @@ -100,6 +100,20 @@ def test_400_message_labelled_non_retryable(self): # Existing remediation text preserved (regression). assert "rejected the request (400)" in msg + def test_400_invalid_model_names_the_proxy_configuration(self): + exc = _exc_with_status( + "BadRequestError", + 400, + "Error code: 400 - Invalid model name passed in model=custom/kimi-k2.7-code. " + "Call `/v1/models` to view available models for your key.", + ) + with pytest.raises(RuntimeError) as info: + _reraise_with_actionable_message(exc, "custom/kimi-k2.7-code") + + msg = str(info.value) + assert "config/litellm.yaml" in msg + assert "/v1/models" in msg + def test_401_message_labelled_non_retryable(self): exc = _exc_with_status("AuthenticationError", 401, "Error code: 401 - invalid_api_key") with pytest.raises(RuntimeError) as info: From 317e218eb0d1a0a403ce151c6ea663fd50beaa01 Mon Sep 17 00:00:00 2001 From: NetVar <89574102+NetVar1337@users.noreply.github.com> Date: Sun, 12 Jul 2026 18:36:23 +0200 Subject: [PATCH 2/4] fix(llm): explain invalid model routes --- packages/decepticon/decepticon/llm/factory.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/decepticon/decepticon/llm/factory.py b/packages/decepticon/decepticon/llm/factory.py index 31bc98fe..902f5ec1 100644 --- a/packages/decepticon/decepticon/llm/factory.py +++ b/packages/decepticon/decepticon/llm/factory.py @@ -1372,6 +1372,14 @@ def _reraise_with_actionable_message(exc: Exception, model_name: str) -> None: f"Underlying: {safe_msg}" ) from exc + if "invalid model name" in msg_lower: + raise RuntimeError( + f"{fatal_prefix}Model '{model_name}' is not available through the LiteLLM " + f"proxy. Verify its route in config/litellm.yaml and query the proxy's " + f"/v1/models endpoint for the models available to your key.\n" + f"Underlying: {safe_msg}" + ) from exc + if "badrequest" in err_type.lower() or "code: 400" in msg_lower: raise RuntimeError( f"{fatal_prefix}Model '{model_name}' rejected the request (400). " From 6bbd3128f2f46ebf07fcb8216c275c4120e5d26d Mon Sep 17 00:00:00 2001 From: NetVar <89574102+NetVar1337@users.noreply.github.com> Date: Sun, 12 Jul 2026 19:58:48 +0200 Subject: [PATCH 3/4] test(llm): constrain invalid model remediation --- .../unit/llm/test_factory_classify_errors.py | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/decepticon/tests/unit/llm/test_factory_classify_errors.py b/packages/decepticon/tests/unit/llm/test_factory_classify_errors.py index 658e2741..ad140686 100644 --- a/packages/decepticon/tests/unit/llm/test_factory_classify_errors.py +++ b/packages/decepticon/tests/unit/llm/test_factory_classify_errors.py @@ -100,19 +100,53 @@ def test_400_message_labelled_non_retryable(self): # Existing remediation text preserved (regression). assert "rejected the request (400)" in msg - def test_400_invalid_model_names_the_proxy_configuration(self): + def test_400_invalid_model_names_static_and_dynamic_proxy_configuration(self): exc = _exc_with_status( "BadRequestError", 400, "Error code: 400 - Invalid model name passed in model=custom/kimi-k2.7-code. " - "Call `/v1/models` to view available models for your key.", + "Authorization: Bearer ***. Call `/v1/models` to view " + "available models for your key.", ) with pytest.raises(RuntimeError) as info: _reraise_with_actionable_message(exc, "custom/kimi-k2.7-code") msg = str(info.value) + assert "non-retryable provider error" in msg assert "config/litellm.yaml" in msg + assert "dynamic environment" in msg + assert "DECEPTICON_MODEL" in msg + assert "restart" in msg.lower() + assert "startup logs" in msg.lower() assert "/v1/models" in msg + assert "ROUTELEAKTOKEN12345" not in msg + assert "[REDACTED]" in msg + + def test_503_invalid_model_text_is_not_reclassified_as_a_route_error(self): + exc = _exc_with_status( + "ServiceUnavailableError", + 503, + "Error code: 503 - upstream temporarily returned Invalid model name", + ) + assert _reraise_with_actionable_message(exc, "custom/kimi-k2.7-code") is None + + def test_value_error_invalid_model_text_passes_through(self): + exc = ValueError("parser captured provider text: Invalid model name") + assert _reraise_with_actionable_message(exc, "custom/kimi-k2.7-code") is None + + def test_composite_400_invalid_model_precedes_no_fallback_guidance(self): + exc = _exc_with_status( + "BadRequestError", + 400, + "Error code: 400 - Invalid model name passed in model=custom/kimi-k2.7-code. " + "No fallback model group found for original model_group=custom/kimi-k2.7-code", + ) + with pytest.raises(RuntimeError) as info: + _reraise_with_actionable_message(exc, "custom/kimi-k2.7-code") + + msg = str(info.value) + assert "not available through the LiteLLM proxy" in msg + assert "another auth method" not in msg def test_401_message_labelled_non_retryable(self): exc = _exc_with_status("AuthenticationError", 401, "Error code: 401 - invalid_api_key") From 94f09948053e57267a96a54cba72f3c8ab1509a9 Mon Sep 17 00:00:00 2001 From: NetVar <89574102+NetVar1337@users.noreply.github.com> Date: Sun, 12 Jul 2026 20:06:05 +0200 Subject: [PATCH 4/4] fix(llm): constrain invalid model remediation --- packages/decepticon/decepticon/llm/factory.py | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/decepticon/decepticon/llm/factory.py b/packages/decepticon/decepticon/llm/factory.py index 902f5ec1..9cbe5392 100644 --- a/packages/decepticon/decepticon/llm/factory.py +++ b/packages/decepticon/decepticon/llm/factory.py @@ -1359,6 +1359,25 @@ def _reraise_with_actionable_message(exc: Exception, model_name: str) -> None: # but interpolate the scrubbed copy so an echoed credential never reaches # the user-facing message — see _redact_secrets. safe_msg = _redact_secrets(msg) + status = getattr(exc, "status_code", None) + if status is None: + status = getattr(getattr(exc, "response", None), "status_code", None) + status_match = _STATUS_IN_MSG.search(msg) + is_bad_request = ( + "badrequest" in err_type.lower() + or status == 400 + or (status_match is not None and int(status_match.group(1)) == 400) + ) + + if is_bad_request and "invalid model name" in msg_lower: + raise RuntimeError( + f"{fatal_prefix}Model '{model_name}' is not available through the LiteLLM " + f"proxy. Verify its static route in config/litellm.yaml or dynamic environment " + f"configuration (DECEPTICON_MODEL*, DECEPTICON_LITELLM_MODELS, " + f"CUSTOM_OPENAI_*, or OLLAMA_MODEL). Restart the proxy, inspect its startup " + f"logs, and query /v1/models for the models available to your key.\n" + f"Underlying: {safe_msg}" + ) from exc # LiteLLM puts a recognizable prefix in the inner message when the # proxy ran out of fallback options for a model_group — issue #107. @@ -1372,15 +1391,7 @@ def _reraise_with_actionable_message(exc: Exception, model_name: str) -> None: f"Underlying: {safe_msg}" ) from exc - if "invalid model name" in msg_lower: - raise RuntimeError( - f"{fatal_prefix}Model '{model_name}' is not available through the LiteLLM " - f"proxy. Verify its route in config/litellm.yaml and query the proxy's " - f"/v1/models endpoint for the models available to your key.\n" - f"Underlying: {safe_msg}" - ) from exc - - if "badrequest" in err_type.lower() or "code: 400" in msg_lower: + if is_bad_request: raise RuntimeError( f"{fatal_prefix}Model '{model_name}' rejected the request (400). " f"This usually means a parameter the model no longer supports "