From f6ead49afe215970176919947ada37ff7e80c8d3 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 28 Jan 2026 17:25:26 +0530 Subject: [PATCH] Add Prompt caching and reasoning support for MiniMax, GLM, Xiaomi --- litellm/llms/minimax/chat/transformation.py | 29 +- .../llms/openrouter/chat/transformation.py | 4 + litellm/llms/zai/chat/transformation.py | 16 +- model_prices_and_context_window.json | 12 +- .../llms/test_cache_control_and_reasoning.py | 281 ++++++++++++++++++ 5 files changed, 337 insertions(+), 5 deletions(-) create mode 100644 tests/test_litellm/llms/test_cache_control_and_reasoning.py diff --git a/litellm/llms/minimax/chat/transformation.py b/litellm/llms/minimax/chat/transformation.py index ed80ff8aed1..3e9dc0209f2 100644 --- a/litellm/llms/minimax/chat/transformation.py +++ b/litellm/llms/minimax/chat/transformation.py @@ -1,11 +1,12 @@ """ MiniMax OpenAI transformation config - extends OpenAI chat config for MiniMax's OpenAI-compatible API """ -from typing import Optional +from typing import List, Optional, Tuple import litellm from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig from litellm.secret_managers.main import get_secret_str +from litellm.types.llms.openai import AllMessageValues, ChatCompletionToolParam class MinimaxChatConfig(OpenAIGPTConfig): @@ -73,11 +74,33 @@ class MinimaxChatConfig(OpenAIGPTConfig): else: return f"{base_url}/v1/chat/completions" + def remove_cache_control_flag_from_messages_and_tools( + self, + model: str, + messages: List[AllMessageValues], + tools: Optional[List[ChatCompletionToolParam]] = None, + ) -> Tuple[List[AllMessageValues], Optional[List[ChatCompletionToolParam]]]: + """ + Override to preserve cache_control for MiniMax. + MiniMax supports cache_control - don't strip it. + """ + # MiniMax supports cache_control, so return messages and tools unchanged + return messages, tools + def get_supported_openai_params(self, model: str) -> list: """ Get supported OpenAI parameters for MiniMax. - Adds reasoning_split to the list of supported params. + Adds reasoning_split and thinking to the list of supported params. """ base_params = super().get_supported_openai_params(model=model) - return base_params + ["reasoning_split"] + additional_params = ["reasoning_split"] + + # Add thinking parameter if model supports reasoning + try: + if litellm.supports_reasoning(model=model, custom_llm_provider="minimax"): + additional_params.append("thinking") + except Exception: + pass + + return base_params + additional_params diff --git a/litellm/llms/openrouter/chat/transformation.py b/litellm/llms/openrouter/chat/transformation.py index b5610852fd2..e3770dbbf49 100644 --- a/litellm/llms/openrouter/chat/transformation.py +++ b/litellm/llms/openrouter/chat/transformation.py @@ -26,6 +26,9 @@ class CacheControlSupportedModels(str, Enum): """Models that support cache_control in content blocks.""" CLAUDE = "claude" GEMINI = "gemini" + MINIMAX = "minimax" + GLM = "glm" + ZAI = "z-ai" class OpenrouterConfig(OpenAIGPTConfig): @@ -39,6 +42,7 @@ class OpenrouterConfig(OpenAIGPTConfig): model=model, custom_llm_provider="openrouter" ) or litellm.supports_reasoning(model=model): supported_params.append("reasoning_effort") + supported_params.append("thinking") except Exception: pass return list(dict.fromkeys(supported_params)) diff --git a/litellm/llms/zai/chat/transformation.py b/litellm/llms/zai/chat/transformation.py index 4380256f0a4..fb1d67df357 100644 --- a/litellm/llms/zai/chat/transformation.py +++ b/litellm/llms/zai/chat/transformation.py @@ -1,6 +1,7 @@ -from typing import Optional, Tuple +from typing import List, Optional, Tuple from litellm.secret_managers.main import get_secret_str +from litellm.types.llms.openai import AllMessageValues, ChatCompletionToolParam from ...openai.chat.gpt_transformation import OpenAIGPTConfig @@ -19,6 +20,19 @@ class ZAIChatConfig(OpenAIGPTConfig): dynamic_api_key = api_key or get_secret_str("ZAI_API_KEY") return api_base, dynamic_api_key + def remove_cache_control_flag_from_messages_and_tools( + self, + model: str, + messages: List[AllMessageValues], + tools: Optional[List[ChatCompletionToolParam]] = None, + ) -> Tuple[List[AllMessageValues], Optional[List[ChatCompletionToolParam]]]: + """ + Override to preserve cache_control for GLM/ZAI. + GLM supports cache_control - don't strip it. + """ + # GLM/ZAI supports cache_control, so return messages and tools unchanged + return messages, tools + def get_supported_openai_params(self, model: str) -> list: base_params = [ "max_tokens", diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 4ac4159558f..b9ed7b7bb76 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -20628,6 +20628,7 @@ "supports_function_calling": true, "supports_tool_choice": true, "supports_prompt_caching": true, + "supports_reasoning": true, "supports_system_messages": true, "max_input_tokens": 1000000, "max_output_tokens": 8192 @@ -20642,6 +20643,7 @@ "supports_function_calling": true, "supports_tool_choice": true, "supports_prompt_caching": true, + "supports_reasoning": true, "supports_system_messages": true, "max_input_tokens": 1000000, "max_output_tokens": 8192 @@ -20656,6 +20658,7 @@ "supports_function_calling": true, "supports_tool_choice": true, "supports_prompt_caching": true, + "supports_reasoning": true, "supports_system_messages": true, "max_input_tokens": 200000, "max_output_tokens": 8192 @@ -23540,7 +23543,7 @@ "mode": "chat", "output_cost_per_token": 1.02e-06, "supports_function_calling": true, - "supports_prompt_caching": false, + "supports_prompt_caching": true, "supports_reasoning": true, "supports_tool_choice": true }, @@ -24232,6 +24235,7 @@ "output_cost_per_token": 1.75e-06, "source": "https://openrouter.ai/z-ai/glm-4.6", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_reasoning": true, "supports_tool_choice": true }, @@ -24245,6 +24249,7 @@ "output_cost_per_token": 1.9e-06, "source": "https://openrouter.ai/z-ai/glm-4.6:exacto", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_reasoning": true, "supports_tool_choice": true }, @@ -30872,11 +30877,14 @@ "max_output_tokens": 128000, "mode": "chat", "supports_function_calling": true, + "supports_prompt_caching": true, "supports_reasoning": true, "supports_tool_choice": true, "source": "https://docs.z.ai/guides/overview/pricing" }, "zai/glm-4.6": { + "cache_creation_input_token_cost": 0, + "cache_read_input_token_cost": 1.1e-07, "input_cost_per_token": 6e-07, "output_cost_per_token": 2.2e-06, "litellm_provider": "zai", @@ -30884,6 +30892,8 @@ "max_output_tokens": 128000, "mode": "chat", "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, "supports_tool_choice": true, "source": "https://docs.z.ai/guides/overview/pricing" }, diff --git a/tests/test_litellm/llms/test_cache_control_and_reasoning.py b/tests/test_litellm/llms/test_cache_control_and_reasoning.py new file mode 100644 index 00000000000..468927cdd49 --- /dev/null +++ b/tests/test_litellm/llms/test_cache_control_and_reasoning.py @@ -0,0 +1,281 @@ +""" +Test cache_control and reasoning parameter support for MiniMax, GLM/ZAI, and OpenRouter. + +This test file verifies the fixes for Issue #19923: +- cache_control is preserved (not stripped) for MiniMax, GLM, and OpenRouter variants +- thinking parameter is supported for reasoning-capable models +- Model metadata correctly reflects capabilities +""" +import os +import sys + +import pytest + +sys.path.insert( + 0, os.path.abspath("../../..") +) # Adds the parent directory to the system path + +from litellm.llms.minimax.chat.transformation import MinimaxChatConfig +from litellm.llms.openrouter.chat.transformation import OpenrouterConfig +from litellm.llms.zai.chat.transformation import ZAIChatConfig + + +def test_minimax_preserves_cache_control_in_messages(): + """MiniMax should NOT strip cache_control from messages.""" + config = MinimaxChatConfig() + + messages = [ + { + "role": "system", + "content": "You are a helpful assistant.", + "cache_control": {"type": "ephemeral"}, + }, + { + "role": "user", + "content": "Hello, world!", + }, + ] + + transformed_messages, _ = config.remove_cache_control_flag_from_messages_and_tools( + model="minimax/MiniMax-M2.1", messages=messages + ) + + # cache_control should be preserved + assert transformed_messages[0].get("cache_control") == {"type": "ephemeral"} + + +def test_minimax_preserves_cache_control_in_tools(): + """MiniMax should NOT strip cache_control from tools.""" + config = MinimaxChatConfig() + + tools = [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get weather information", + "parameters": {"type": "object", "properties": {}}, + }, + "cache_control": {"type": "ephemeral"}, + } + ] + + _, transformed_tools = config.remove_cache_control_flag_from_messages_and_tools( + model="minimax/MiniMax-M2.1", messages=[], tools=tools + ) + + # cache_control should be preserved + assert transformed_tools[0].get("cache_control") == {"type": "ephemeral"} + + +def test_minimax_supports_thinking_param(): + """MiniMax reasoning models should support thinking parameter.""" + config = MinimaxChatConfig() + + supported_params = config.get_supported_openai_params( + model="minimax/MiniMax-M2.1" + ) + + # thinking should be in supported params for reasoning models + assert "thinking" in supported_params + # reasoning_split should also be supported + assert "reasoning_split" in supported_params + + +def test_zai_preserves_cache_control_in_messages(): + """ZAI should NOT strip cache_control from messages.""" + config = ZAIChatConfig() + + messages = [ + { + "role": "system", + "content": "You are a helpful assistant.", + "cache_control": {"type": "ephemeral"}, + }, + { + "role": "user", + "content": "Hello, world!", + }, + ] + + transformed_messages, _ = config.remove_cache_control_flag_from_messages_and_tools( + model="zai/glm-4.7", messages=messages + ) + + # cache_control should be preserved + assert transformed_messages[0].get("cache_control") == {"type": "ephemeral"} + + +def test_zai_preserves_cache_control_in_tools(): + """ZAI should NOT strip cache_control from tools.""" + config = ZAIChatConfig() + + tools = [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get weather information", + "parameters": {"type": "object", "properties": {}}, + }, + "cache_control": {"type": "ephemeral"}, + } + ] + + _, transformed_tools = config.remove_cache_control_flag_from_messages_and_tools( + model="zai/glm-4.7", messages=[], tools=tools + ) + + # cache_control should be preserved + assert transformed_tools[0].get("cache_control") == {"type": "ephemeral"} + + +def test_zai_supports_thinking_param_for_reasoning_models(): + """ZAI reasoning models (glm-4.7, glm-4.6) should support thinking parameter.""" + config = ZAIChatConfig() + + # glm-4.7 supports reasoning + supported_params_47 = config.get_supported_openai_params(model="zai/glm-4.7") + assert "thinking" in supported_params_47 + + # glm-4.6 supports reasoning + supported_params_46 = config.get_supported_openai_params(model="zai/glm-4.6") + assert "thinking" in supported_params_46 + + +def test_openrouter_minimax_supports_cache_control(): + """OpenRouter should preserve cache_control for MiniMax models.""" + config = OpenrouterConfig() + + messages = [ + { + "role": "user", + "content": "Hello, world!", + "cache_control": {"type": "ephemeral"}, + } + ] + + # Test that cache_control is not removed + transformed_messages, _ = config.remove_cache_control_flag_from_messages_and_tools( + model="openrouter/minimax/minimax-m2", messages=messages + ) + + # The method should preserve cache_control for minimax models + assert transformed_messages[0].get("cache_control") == {"type": "ephemeral"} + + +def test_openrouter_glm_supports_cache_control(): + """OpenRouter should preserve cache_control for GLM models.""" + config = OpenrouterConfig() + + messages = [ + { + "role": "user", + "content": "Hello, world!", + "cache_control": {"type": "ephemeral"}, + } + ] + + # Test that cache_control is not removed for GLM models + transformed_messages, _ = config.remove_cache_control_flag_from_messages_and_tools( + model="openrouter/z-ai/glm-4.6", messages=messages + ) + + # The method should preserve cache_control for GLM models + assert transformed_messages[0].get("cache_control") == {"type": "ephemeral"} + + +def test_openrouter_deepseek_strips_cache_control(): + """OpenRouter should still strip cache_control for non-supported models.""" + config = OpenrouterConfig() + + messages = [ + { + "role": "user", + "content": "Hello, world!", + "cache_control": {"type": "ephemeral"}, + } + ] + + # DeepSeek doesn't support cache_control, so it should be stripped + transformed_messages, _ = config.remove_cache_control_flag_from_messages_and_tools( + model="openrouter/deepseek/deepseek-chat", messages=messages + ) + + # cache_control should be removed for non-supported models + assert transformed_messages[0].get("cache_control") is None + + +def test_openrouter_minimax_transform_moves_cache_control_to_content(): + """OpenRouter should move cache_control to content blocks for MiniMax.""" + config = OpenrouterConfig() + + messages = [ + { + "role": "user", + "content": "Analyze this data", + "cache_control": {"type": "ephemeral"}, + } + ] + + transformed_request = config.transform_request( + model="openrouter/minimax/minimax-m2", + messages=messages, + optional_params={}, + litellm_params={}, + headers={}, + ) + + # cache_control should be moved to content blocks + assert "messages" in transformed_request + user_message = transformed_request["messages"][0] + assert isinstance(user_message["content"], list) + assert user_message["content"][0]["cache_control"] == {"type": "ephemeral"} + # Message-level cache_control should be removed + assert "cache_control" not in user_message + + +def test_openrouter_glm_transform_moves_cache_control_to_content(): + """OpenRouter should move cache_control to content blocks for GLM.""" + config = OpenrouterConfig() + + messages = [ + { + "role": "user", + "content": "Analyze this data", + "cache_control": {"type": "ephemeral"}, + } + ] + + transformed_request = config.transform_request( + model="openrouter/z-ai/glm-4.6", + messages=messages, + optional_params={}, + litellm_params={}, + headers={}, + ) + + # cache_control should be moved to content blocks + assert "messages" in transformed_request + user_message = transformed_request["messages"][0] + assert isinstance(user_message["content"], list) + assert user_message["content"][0]["cache_control"] == {"type": "ephemeral"} + + +def test_openrouter_supports_thinking_param_for_reasoning_models(): + """OpenRouter should support thinking parameter for reasoning-capable models.""" + config = OpenrouterConfig() + + # Test MiniMax (supports reasoning) + supported_params_minimax = config.get_supported_openai_params( + model="openrouter/minimax/minimax-m2" + ) + assert "thinking" in supported_params_minimax + assert "reasoning_effort" in supported_params_minimax + + # Test GLM (supports reasoning) + supported_params_glm = config.get_supported_openai_params( + model="openrouter/z-ai/glm-4.6" + ) + assert "thinking" in supported_params_glm + assert "reasoning_effort" in supported_params_glm