diff --git a/litellm/llms/openai_like/chat/handler.py b/litellm/llms/openai_like/chat/handler.py index 821fc9b7f15..125006da277 100644 --- a/litellm/llms/openai_like/chat/handler.py +++ b/litellm/llms/openai_like/chat/handler.py @@ -10,6 +10,7 @@ from typing import Any, Callable, Optional, Union import httpx import litellm +from litellm._logging import verbose_logger from litellm import LlmProviders from litellm.llms.bedrock.chat.invoke_handler import MockResponseIterator from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler @@ -20,6 +21,7 @@ from litellm.types.utils import CustomStreamingDecoder, ModelResponse from litellm.utils import CustomStreamWrapper, ProviderConfigManager from ..common_utils import OpenAILikeBase, OpenAILikeError +from ..json_loader import JSONProviderRegistry from .transformation import OpenAILikeChatConfig @@ -112,6 +114,25 @@ class OpenAILikeChatHandler(OpenAILikeBase): def __init__(self, **kwargs): super().__init__(**kwargs) + @staticmethod + def _drop_provider_unsupported_params( + custom_llm_provider: str, optional_params: dict, extra_body: dict + ) -> None: + provider = JSONProviderRegistry.get(custom_llm_provider) + if provider is None: + return + + for param in provider.unsupported_params: + removed_optional_param = optional_params.pop(param, None) + removed_extra_body_param = extra_body.pop(param, None) + if ( + removed_optional_param is not None + or removed_extra_body_param is not None + ): + verbose_logger.debug( + f"Dropping unsupported param '{param}' for provider '{custom_llm_provider}'" + ) + async def acompletion_stream_function( self, model: str, @@ -254,9 +275,14 @@ class OpenAILikeChatHandler(OpenAILikeBase): ) stream: bool = optional_params.pop("stream", None) or False - extra_body = optional_params.pop("extra_body", {}) + extra_body = optional_params.pop("extra_body", {}) or {} json_mode = optional_params.pop("json_mode", None) optional_params.pop("max_retries", None) + self._drop_provider_unsupported_params( + custom_llm_provider=custom_llm_provider, + optional_params=optional_params, + extra_body=extra_body, + ) if not fake_stream: optional_params["stream"] = stream diff --git a/litellm/llms/openai_like/json_loader.py b/litellm/llms/openai_like/json_loader.py index c6ff0f7a394..b9c604ee057 100644 --- a/litellm/llms/openai_like/json_loader.py +++ b/litellm/llms/openai_like/json_loader.py @@ -21,6 +21,7 @@ class SimpleProviderConfig: self.param_mappings = data.get("param_mappings", {}) self.constraints = data.get("constraints", {}) self.special_handling = data.get("special_handling", {}) + self.unsupported_params = data.get("unsupported_params", []) self.supported_endpoints = data.get("supported_endpoints", []) diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index b5e5aa4ea28..e662fe85d13 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -22,6 +22,9 @@ "xiaomi_mimo": { "base_url": "https://api.xiaomimimo.com/v1", "api_key_env": "XIAOMI_MIMO_API_KEY", + "unsupported_params": [ + "output_config" + ], "param_mappings": { "max_completion_tokens": "max_tokens" } diff --git a/tests/test_litellm/llms/openai_like/test_xiaomi_mimo.py b/tests/test_litellm/llms/openai_like/test_xiaomi_mimo.py index 8104fb12943..92412902c18 100644 --- a/tests/test_litellm/llms/openai_like/test_xiaomi_mimo.py +++ b/tests/test_litellm/llms/openai_like/test_xiaomi_mimo.py @@ -4,6 +4,7 @@ Related to issue #18794 """ import os +import json import sys from unittest.mock import MagicMock, patch @@ -46,6 +47,7 @@ class TestXiaomiMiMoProviderConfig: assert xiaomi_mimo.base_url == "https://api.xiaomimimo.com/v1" assert xiaomi_mimo.api_key_env == "XIAOMI_MIMO_API_KEY" assert xiaomi_mimo.param_mappings.get("max_completion_tokens") == "max_tokens" + assert "output_config" in xiaomi_mimo.unsupported_params def test_xiaomi_mimo_provider_resolution(self): """Test that provider resolution finds xiaomi_mimo""" @@ -83,6 +85,132 @@ class TestXiaomiMiMoProviderConfig: assert len(router.model_list) == 1 assert router.model_list[0]["model_name"] == "mimo-v2-flash" + def test_xiaomi_mimo_drops_output_config_from_request_body(self): + """Xiaomi MiMo rejects Claude-only output_config; do not forward it.""" + from litellm.llms.custom_httpx.http_handler import HTTPHandler + from litellm.llms.openai_like.chat.handler import OpenAILikeChatHandler + from litellm.types.utils import ModelResponse + + client = HTTPHandler() + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "application/json"} + mock_response.json.return_value = { + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 0, + "model": "mimo-v2-flash", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "ok"}, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, + } + mock_response.raise_for_status.return_value = None + + logging_obj = MagicMock() + logging_obj.model_call_details = {} + + with patch.object(client, "post", return_value=mock_response) as mock_post: + OpenAILikeChatHandler().completion( + model="mimo-v2-flash", + messages=[{"role": "user", "content": "hi"}], + api_base="https://api.xiaomimimo.com/v1", + custom_llm_provider="xiaomi_mimo", + custom_prompt_dict={}, + model_response=ModelResponse(), + print_verbose=lambda *args, **kwargs: None, + encoding=None, + api_key="test-key", + logging_obj=logging_obj, + optional_params={ + "output_config": {"effort": "medium"}, + "extra_body": {"output_config": {"effort": "medium"}}, + }, + client=client, + ) + + request_body = json.loads(mock_post.call_args.kwargs["data"]) + assert "output_config" not in request_body + assert request_body["model"] == "mimo-v2-flash" + assert request_body["messages"] == [{"role": "user", "content": "hi"}] + + def test_xiaomi_mimo_handles_none_extra_body(self): + """An explicit extra_body=None should not break unsupported-param filtering.""" + from litellm.llms.custom_httpx.http_handler import HTTPHandler + from litellm.llms.openai_like.chat.handler import OpenAILikeChatHandler + from litellm.types.utils import ModelResponse + + client = HTTPHandler() + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.headers = {"content-type": "application/json"} + mock_response.json.return_value = { + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 0, + "model": "mimo-v2-flash", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "ok"}, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, + } + mock_response.raise_for_status.return_value = None + + logging_obj = MagicMock() + logging_obj.model_call_details = {} + + with patch.object(client, "post", return_value=mock_response) as mock_post: + OpenAILikeChatHandler().completion( + model="mimo-v2-flash", + messages=[{"role": "user", "content": "hi"}], + api_base="https://api.xiaomimimo.com/v1", + custom_llm_provider="xiaomi_mimo", + custom_prompt_dict={}, + model_response=ModelResponse(), + print_verbose=lambda *args, **kwargs: None, + encoding=None, + api_key="test-key", + logging_obj=logging_obj, + optional_params={ + "extra_body": None, + "output_config": {"effort": "medium"}, + }, + client=client, + ) + + request_body = json.loads(mock_post.call_args.kwargs["data"]) + assert "output_config" not in request_body + assert request_body["model"] == "mimo-v2-flash" + + def test_xiaomi_mimo_logs_dropped_output_config(self): + """Dropped provider params should be observable in debug logs.""" + from litellm.llms.openai_like.chat.handler import OpenAILikeChatHandler + + optional_params = {"output_config": {"effort": "medium"}} + extra_body = {} + + with patch( + "litellm.llms.openai_like.chat.handler.verbose_logger.debug" + ) as mock_debug: + OpenAILikeChatHandler._drop_provider_unsupported_params( + custom_llm_provider="xiaomi_mimo", + optional_params=optional_params, + extra_body=extra_body, + ) + + assert optional_params == {} + mock_debug.assert_called_once_with( + "Dropping unsupported param 'output_config' for provider 'xiaomi_mimo'" + ) + class TestXiaomiMiMoIntegration: """Integration tests for Xiaomi MiMo provider"""