diff --git a/litellm/llms/openai_like/dynamic_config.py b/litellm/llms/openai_like/dynamic_config.py index fac453447fa..abe9974d550 100644 --- a/litellm/llms/openai_like/dynamic_config.py +++ b/litellm/llms/openai_like/dynamic_config.py @@ -4,6 +4,8 @@ Dynamic configuration class generator for JSON-based providers. from typing import Any, Coroutine, List, Literal, Optional, Tuple, Union, overload +import httpx + from litellm._logging import verbose_logger from litellm.litellm_core_utils.prompt_templates.common_utils import ( handle_messages_with_content_list_to_str_conversion, @@ -165,6 +167,61 @@ def create_config_class(provider: SimpleProviderConfig): return optional_params + def should_retry_llm_api_inside_llm_translation_on_http_error( + self, e: httpx.HTTPStatusError, litellm_params: dict + ) -> bool: + """ + Retry on 422 UnprocessableEntity when drop_params is enabled. + + OpenAI-compatible providers may reject unknown parameters. + When drop_params is set, strip the offending params and retry. + """ + import litellm as _litellm + + should_drop = litellm_params.get("drop_params") or _litellm.drop_params + if should_drop and e.response.status_code == 422: + error_text = e.response.text + if "Extra inputs are not permitted" in error_text: + return True + return super().should_retry_llm_api_inside_llm_translation_on_http_error( + e=e, litellm_params=litellm_params + ) + + @property + def max_retry_on_unprocessable_entity_error(self) -> int: + return 2 + + def transform_request_on_unprocessable_entity_error( + self, e: httpx.HTTPStatusError, request_data: dict + ) -> dict: + """ + On 422, attempt to extract the offending field name from the error + response and remove it from the request data before retrying. + """ + import json as _json + + try: + error_body = _json.loads(e.response.text) + error_msg = str(error_body) + except Exception: + error_msg = e.response.text + + if "Extra inputs are not permitted" in error_msg: + # Try to extract field name from error message pattern like: + # "field_name: Extra inputs are not permitted" + for line in error_msg.split(","): + line = line.strip() + if "Extra inputs are not permitted" in line: + field = line.split(":")[0].strip().strip("'\"{ ") + if field and field in request_data: + verbose_logger.debug( + f"Removing unsupported param '{field}' from " + f"request to {provider.slug} on retry" + ) + request_data.pop(field, None) + + return request_data + @property def custom_llm_provider(self) -> Optional[str]: return provider.slug diff --git a/litellm/types/utils.py b/litellm/types/utils.py index ed29d49fc29..7b8abcbbdd0 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3327,6 +3327,13 @@ class LlmProviders(str, Enum): NANOGPT = "nano-gpt" POE = "poe" CHUTES = "chutes" + AVIAN = "avian" + ABLITERATION = "abliteration" + LLAMAGATE = "llamagate" + GMI = "gmi" + SARVAM = "sarvam" + AIHUBMIX = "aihubmix" + VENICEAI = "veniceai" XIAOMI_MIMO = "xiaomi_mimo" LITELLM_AGENT = "litellm_agent" CURSOR = "cursor" diff --git a/tests/litellm/llms/openai_like/test_avian_provider.py b/tests/litellm/llms/openai_like/test_avian_provider.py index 410b68a8341..7271ef4d434 100644 --- a/tests/litellm/llms/openai_like/test_avian_provider.py +++ b/tests/litellm/llms/openai_like/test_avian_provider.py @@ -5,6 +5,9 @@ Unit tests for the Avian OpenAI-like provider. import os import sys +import httpx +import pytest + sys.path.insert( 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../..")) ) @@ -48,3 +51,63 @@ def test_avian_complete_url_appends_endpoint(): stream=False, ) assert url == f"{AVIAN_BASE_URL}/chat/completions" + + +def test_avian_in_provider_list(): + from litellm.types.utils import LlmProviders + + assert LlmProviders.AVIAN.value == "avian" + + +def test_avian_retry_on_422_with_drop_params(): + config = _get_config() + mock_request = httpx.Request(method="POST", url=AVIAN_BASE_URL) + mock_response = httpx.Response( + status_code=422, + text='{"detail": "max_completion_tokens: Extra inputs are not permitted"}', + request=mock_request, + ) + error = httpx.HTTPStatusError( + message="422", request=mock_request, response=mock_response + ) + assert config.should_retry_llm_api_inside_llm_translation_on_http_error( + e=error, litellm_params={"drop_params": True} + ) + + +def test_avian_no_retry_without_drop_params(): + config = _get_config() + mock_request = httpx.Request(method="POST", url=AVIAN_BASE_URL) + mock_response = httpx.Response( + status_code=422, + text='{"detail": "Extra inputs are not permitted"}', + request=mock_request, + ) + error = httpx.HTTPStatusError( + message="422", request=mock_request, response=mock_response + ) + assert not config.should_retry_llm_api_inside_llm_translation_on_http_error( + e=error, litellm_params={} + ) + + +def test_avian_max_retry_count(): + config = _get_config() + assert config.max_retry_on_unprocessable_entity_error == 2 + + +@pytest.mark.flaky(retries=3, delay=1) +@pytest.mark.skipif( + not os.environ.get("AVIAN_API_KEY"), + reason="AVIAN_API_KEY not set", +) +def test_avian_chat_completion(): + """Live API test — requires AVIAN_API_KEY.""" + import litellm + + response = litellm.completion( + model="avian/deepseek/deepseek-v3.2", + messages=[{"role": "user", "content": "Say hello in one word."}], + max_tokens=10, + ) + assert response.choices[0].message.content is not None