From ef1cde433ea7c6dd1515de06c6d0d748fae4a197 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:22:14 -0700 Subject: [PATCH 1/5] fix: add moonshot/kimi-k3 to the cost map models.litellm.ai and released litellm versions read model_prices_and_context_window.json from main at runtime, so Kimi K3 is missing from the hosted catalog even though the entry is in review for litellm_internal_staging in #37552. This copies that entry onto main so the catalog picks it up on its next fetch. Data only: the cost map and its backup copy, no code changes. Pricing matches Moonshot's published rates ($3/M input, $0.30/M cache read, $15/M output, 1,048,576-token context). The fireworks_ai and Azure Foundry kimi-k3 variants are separate work in #37512 and #37658; neither touches the native moonshot/kimi-k3 key. --- .../model_prices_and_context_window_backup.json | 17 +++++++++++++++++ model_prices_and_context_window.json | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 07f9027313b..53d069c4a71 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -30025,6 +30025,23 @@ "supports_video_input": true, "supports_vision": true }, + "moonshot/kimi-k3": { + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "litellm_provider": "moonshot", + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "max_tokens": 1048576, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "source": "https://platform.kimi.ai/docs/pricing/chat-k3", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_video_input": true, + "supports_vision": true + }, "moonshot/kimi-latest": { "cache_read_input_token_cost": 1.5e-07, "deprecation_date": "2026-01-28", diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 07f9027313b..53d069c4a71 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -30025,6 +30025,23 @@ "supports_video_input": true, "supports_vision": true }, + "moonshot/kimi-k3": { + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 3e-06, + "litellm_provider": "moonshot", + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "max_tokens": 1048576, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "source": "https://platform.kimi.ai/docs/pricing/chat-k3", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_video_input": true, + "supports_vision": true + }, "moonshot/kimi-latest": { "cache_read_input_token_cost": 1.5e-07, "deprecation_date": "2026-01-28", From 6a0e7fe10f8463c9333c526efde7eb7c9bb2c63a Mon Sep 17 00:00:00 2001 From: Felipe Rodrigues Gare Carnielli Date: Mon, 24 Aug 2026 14:58:13 -0300 Subject: [PATCH 2/5] fix(tencent): route thinking through extra_body in chat completions Tencent chat completions route through the OpenAI SDK's chat.completions.create(), which raises TypeError on unknown kwargs - so a top-level 'thinking' optional param crashed every reasoning request with a 500 before any HTTP call was made. Nest the resolved thinking object in extra_body instead: the SDK merges extra_body into the top-level JSON payload, so TokenHub still receives the documented thinking field (type/budget_tokens) in the request body. Also align the param mapping with TokenHub's documented behavior: - reasoning_effort="none" now maps to thinking={"type": "disabled"} instead of being dropped (deepseek-v4-* default to thinking enabled, so dropping it never actually disabled thinking) - MiniMax models only accept thinking.type "adaptive"/"disabled", so "enabled" is coerced to "adaptive" instead of returning a 400 Refs: https://www.tencentcloud.com/document/product/1300/82345 --- litellm/llms/tencent/chat/transformation.py | 34 ++++- .../chat/test_tencent_chat_transformation.py | 138 +++++++++++++++++- tests/test_litellm/test_utils.py | 12 +- 3 files changed, 171 insertions(+), 13 deletions(-) diff --git a/litellm/llms/tencent/chat/transformation.py b/litellm/llms/tencent/chat/transformation.py index b1672d93542..08b7c364e92 100644 --- a/litellm/llms/tencent/chat/transformation.py +++ b/litellm/llms/tencent/chat/transformation.py @@ -30,14 +30,38 @@ class TencentChatConfig(OpenAIGPTConfig): thinking_value: Final = optional_params.pop("thinking", None) reasoning_effort: Final = optional_params.pop("reasoning_effort", None) - if thinking_value is not None: - if isinstance(thinking_value, dict): - optional_params["thinking"] = thinking_value - elif reasoning_effort is not None and reasoning_effort != "none": - optional_params["thinking"] = {"type": "enabled"} + thinking: dict | None = None + if isinstance(thinking_value, dict): + thinking = thinking_value + elif reasoning_effort is not None: + # TokenHub recommends explicitly disabling thinking instead of + # relying on per-model defaults (deepseek-v4-* default to enabled). + thinking = {"type": "disabled" if reasoning_effort == "none" else "enabled"} + + if thinking is not None: + thinking = self._normalize_thinking_type_for_model(model=model, thinking=thinking) + # Tencent TokenHub expects `thinking` in the request JSON body, but + # the OpenAI SDK's chat.completions.create() rejects unknown + # top-level kwargs. Route it through `extra_body` so it is merged + # into the payload instead of passed as a keyword argument. + extra_body: Final = optional_params.setdefault("extra_body", {}) + extra_body["thinking"] = thinking return optional_params + @staticmethod + def _normalize_thinking_type_for_model(model: str, thinking: dict) -> dict: + """Coerce `thinking.type` values the model does not accept. + + MiniMax models on TokenHub only accept "adaptive" or "disabled" — + sending "enabled" returns a 400. "adaptive" is the closest semantic + (the model decides when to think), so "enabled" is coerced to it. + Ref: https://www.tencentcloud.com/document/product/1300/82345 + """ + if thinking.get("type") == "enabled" and "minimax" in model.lower(): + return {**thinking, "type": "adaptive"} + return thinking + def _get_openai_compatible_provider_info( self, api_base: str | None, api_key: str | None ) -> tuple[str | None, str | None]: diff --git a/tests/test_litellm/llms/tencent/chat/test_tencent_chat_transformation.py b/tests/test_litellm/llms/tencent/chat/test_tencent_chat_transformation.py index 00a82041c20..806d585a4c9 100644 --- a/tests/test_litellm/llms/tencent/chat/test_tencent_chat_transformation.py +++ b/tests/test_litellm/llms/tencent/chat/test_tencent_chat_transformation.py @@ -45,7 +45,8 @@ def test_map_openai_params_passes_thinking_dict_through(): drop_params=False, ) - assert result["thinking"] == {"type": "enabled", "budget_tokens": 1024} + assert "thinking" not in result + assert result["extra_body"]["thinking"] == {"type": "enabled", "budget_tokens": 1024} def test_map_openai_params_converts_reasoning_effort_to_thinking(): @@ -61,10 +62,11 @@ def test_map_openai_params_converts_reasoning_effort_to_thinking(): drop_params=False, ) - assert result["thinking"] == {"type": "enabled"} + assert "thinking" not in result + assert result["extra_body"]["thinking"] == {"type": "enabled"} -def test_map_openai_params_drops_none_reasoning_effort(): +def test_map_openai_params_none_reasoning_effort_disables_thinking(): config = TencentChatConfig() with patch( "litellm.llms.tencent.chat.transformation.supports_reasoning", @@ -78,6 +80,7 @@ def test_map_openai_params_drops_none_reasoning_effort(): ) assert "thinking" not in result + assert result["extra_body"]["thinking"] == {"type": "disabled"} assert "reasoning_effort" not in result @@ -97,7 +100,8 @@ def test_map_openai_params_thinking_priority_over_reasoning_effort(): drop_params=False, ) - assert result["thinking"] == {"type": "enabled", "budget_tokens": 2048} + assert "thinking" not in result + assert result["extra_body"]["thinking"] == {"type": "enabled", "budget_tokens": 2048} def test_map_openai_params_extracts_thinking_and_effort_from_optional_params(): @@ -109,10 +113,134 @@ def test_map_openai_params_extracts_thinking_and_effort_from_optional_params(): drop_params=False, ) - assert "thinking" in result + assert "thinking" not in result + assert result["extra_body"]["thinking"] == {"type": "enabled"} assert "reasoning_effort" not in result +def test_map_openai_params_merges_into_existing_extra_body(): + config = TencentChatConfig() + result = config.map_openai_params( + non_default_params={}, + optional_params={ + "thinking": {"type": "enabled"}, + "extra_body": {"custom_flag": True}, + }, + model="tencent/deepseek-v4-pro", + drop_params=False, + ) + + assert result["extra_body"] == {"custom_flag": True, "thinking": {"type": "enabled"}} + + +def test_transform_request_never_passes_thinking_as_top_level_kwarg(): + """ + Regression test: tencent routes through the OpenAI SDK's + chat.completions.create(**data), which raises TypeError on unknown kwargs. + `thinking` must be nested inside extra_body, never top-level. + """ + config = TencentChatConfig() + optional_params = config.map_openai_params( + non_default_params={"thinking": {"type": "enabled", "budget_tokens": 1024}}, + optional_params={}, + model="tencent/deepseek-v4-pro", + drop_params=False, + ) + + data = config.transform_request( + model="deepseek-v4-pro", + messages=[{"role": "user", "content": "hi"}], + optional_params=optional_params, + litellm_params={}, + headers={}, + ) + + assert "thinking" not in data + assert data["extra_body"]["thinking"] == {"type": "enabled", "budget_tokens": 1024} + + +class TestMinimaxThinkingCoercion: + """ + MiniMax models on TokenHub only accept thinking.type "adaptive"/"disabled" — + "enabled" returns a 400. Ref: https://www.tencentcloud.com/document/product/1300/82345 + """ + + def test_reasoning_effort_maps_to_adaptive_for_minimax(self): + config = TencentChatConfig() + with patch( + "litellm.llms.tencent.chat.transformation.supports_reasoning", + return_value=True, + ): + result = config.map_openai_params( + non_default_params={"reasoning_effort": "medium"}, + optional_params={}, + model="tencent/minimax-m3", + drop_params=False, + ) + + assert result["extra_body"]["thinking"] == {"type": "adaptive"} + + def test_explicit_enabled_thinking_coerced_to_adaptive_for_minimax(self): + config = TencentChatConfig() + with patch( + "litellm.llms.tencent.chat.transformation.supports_reasoning", + return_value=True, + ): + result = config.map_openai_params( + non_default_params={"thinking": {"type": "enabled", "budget_tokens": 4096}}, + optional_params={}, + model="minimax-m3", + drop_params=False, + ) + + assert result["extra_body"]["thinking"] == {"type": "adaptive", "budget_tokens": 4096} + + def test_disabled_thinking_kept_for_minimax(self): + config = TencentChatConfig() + with patch( + "litellm.llms.tencent.chat.transformation.supports_reasoning", + return_value=True, + ): + result = config.map_openai_params( + non_default_params={"thinking": {"type": "disabled"}}, + optional_params={}, + model="tencent/minimax-m3", + drop_params=False, + ) + + assert result["extra_body"]["thinking"] == {"type": "disabled"} + + def test_none_reasoning_effort_disables_thinking_for_minimax(self): + config = TencentChatConfig() + with patch( + "litellm.llms.tencent.chat.transformation.supports_reasoning", + return_value=True, + ): + result = config.map_openai_params( + non_default_params={"reasoning_effort": "none"}, + optional_params={}, + model="tencent/minimax-m3", + drop_params=False, + ) + + assert result["extra_body"]["thinking"] == {"type": "disabled"} + + def test_non_minimax_model_keeps_enabled(self): + config = TencentChatConfig() + with patch( + "litellm.llms.tencent.chat.transformation.supports_reasoning", + return_value=True, + ): + result = config.map_openai_params( + non_default_params={"reasoning_effort": "high"}, + optional_params={}, + model="tencent/kimi-k3", + drop_params=False, + ) + + assert result["extra_body"]["thinking"] == {"type": "enabled"} + + def test_get_complete_url_default(): config = TencentChatConfig() diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index d655eb96a02..84decd01adc 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -4244,7 +4244,11 @@ class TestGetOptionalParamsTencent: """Tests that tencent provider uses TencentChatConfig for parameter mapping.""" def test_tencent_supports_thinking_param(self): - """Verify get_optional_params for tencent accepts the 'thinking' param.""" + """Verify get_optional_params for tencent accepts the 'thinking' param. + + `thinking` must be nested in extra_body: tencent routes through the + OpenAI SDK's chat.completions.create(), which rejects unknown kwargs. + """ from unittest.mock import patch from litellm.utils import get_optional_params @@ -4258,7 +4262,8 @@ class TestGetOptionalParamsTencent: custom_llm_provider="tencent", thinking={"type": "enabled"}, ) - assert result.get("thinking") == {"type": "enabled"} + assert "thinking" not in result + assert result["extra_body"]["thinking"] == {"type": "enabled"} def test_tencent_supports_reasoning_effort(self): """Verify get_optional_params for tencent converts reasoning_effort to thinking.""" @@ -4275,7 +4280,8 @@ class TestGetOptionalParamsTencent: custom_llm_provider="tencent", reasoning_effort="medium", ) - assert result.get("thinking") == {"type": "enabled"} + assert "thinking" not in result + assert result["extra_body"]["thinking"] == {"type": "enabled"} def test_tencent_supported_params_includes_thinking_and_reasoning_effort(self): """Verify get_supported_openai_params for tencent includes custom params.""" From c6b4cb93b71274a518aff28011f06f8df0d08414 Mon Sep 17 00:00:00 2001 From: Felipe Rodrigues Gare Carnielli Date: Mon, 24 Aug 2026 16:28:36 -0300 Subject: [PATCH 3/5] refactor(tencent): capability-driven thinking coercion Address Greptile review comments and the strict lint budgets: - read supports_adaptive_thinking from the model cost map instead of substring-matching the model name, so aliases and newly onboarded adaptive-only models need no code change - add tencent/minimax-m3 to the pricing JSON (and backup), which also fixes cost tracking for the model - type the thinking/extra_body payloads with ReadOnly TypedDicts - build the merged extra_body without rebinding or in-place mutation --- litellm/llms/tencent/chat/transformation.py | 111 +++++++++++++----- ...odel_prices_and_context_window_backup.json | 20 ++++ model_prices_and_context_window.json | 20 ++++ .../chat/test_tencent_chat_transformation.py | 87 ++++++++++---- 4 files changed, 186 insertions(+), 52 deletions(-) diff --git a/litellm/llms/tencent/chat/transformation.py b/litellm/llms/tencent/chat/transformation.py index 08b7c364e92..283a227943d 100644 --- a/litellm/llms/tencent/chat/transformation.py +++ b/litellm/llms/tencent/chat/transformation.py @@ -3,14 +3,36 @@ Translates from OpenAI's `/v1/chat/completions` to Tencent TokenHub's OpenAI-compatible endpoint. """ -from typing import Final +from collections.abc import Mapping +from typing import Final, TypedDict +from typing_extensions import ReadOnly + +import litellm from litellm.secret_managers.main import get_secret_str from litellm.utils import supports_reasoning from ...openai.chat.gpt_transformation import OpenAIGPTConfig +class ThinkingPayload(TypedDict, total=False): + """Tencent TokenHub `thinking` object. + + `type` ("enabled"/"disabled"/"adaptive") is required by TokenHub when the + object is passed; `budget_tokens` is auto-filled server-side when omitted. + Ref: https://www.tencentcloud.com/document/product/1300/82345 + """ + + type: ReadOnly[str] + budget_tokens: ReadOnly[int] + + +class TencentExtraBody(TypedDict, total=False): + """`extra_body` payload for TokenHub chat requests.""" + + thinking: ReadOnly[Mapping[str, object]] + + class TencentChatConfig(OpenAIGPTConfig): def get_supported_openai_params(self, model: str) -> list: params: Final = super().get_supported_openai_params(model) @@ -25,42 +47,75 @@ class TencentChatConfig(OpenAIGPTConfig): model: str, drop_params: bool, ) -> dict: - optional_params = super().map_openai_params(non_default_params, optional_params, model, drop_params) + mapped_params: Final = super().map_openai_params(non_default_params, optional_params, model, drop_params) - thinking_value: Final = optional_params.pop("thinking", None) - reasoning_effort: Final = optional_params.pop("reasoning_effort", None) + thinking_value: Final = mapped_params.pop("thinking", None) + reasoning_effort: Final = mapped_params.pop("reasoning_effort", None) - thinking: dict | None = None + thinking: Final = self._resolve_thinking_payload( + model=model, + thinking_value=thinking_value, + reasoning_effort=reasoning_effort, + ) + if thinking is None: + return mapped_params + + # TokenHub expects `thinking` in the request JSON body, but the OpenAI + # SDK's chat.completions.create() rejects unknown top-level kwargs, so + # it travels via `extra_body`, which the SDK merges into the payload. + existing_extra_body: Final = mapped_params.pop("extra_body", None) + if isinstance(existing_extra_body, dict): + merged_extra_body: Final[TencentExtraBody] = {**existing_extra_body, "thinking": thinking} + else: + merged_extra_body: Final[TencentExtraBody] = {"thinking": thinking} + mapped_params["extra_body"] = merged_extra_body + return mapped_params + + @classmethod + def _resolve_thinking_payload( + cls, + model: str, + thinking_value: object, + reasoning_effort: object, + ) -> Mapping[str, object] | None: if isinstance(thinking_value, dict): - thinking = thinking_value - elif reasoning_effort is not None: - # TokenHub recommends explicitly disabling thinking instead of + return cls._coerce_thinking_type_for_model(model=model, thinking=thinking_value) + if isinstance(reasoning_effort, str): + # TokenHub recommends explicitly disabling thinking rather than # relying on per-model defaults (deepseek-v4-* default to enabled). - thinking = {"type": "disabled" if reasoning_effort == "none" else "enabled"} - - if thinking is not None: - thinking = self._normalize_thinking_type_for_model(model=model, thinking=thinking) - # Tencent TokenHub expects `thinking` in the request JSON body, but - # the OpenAI SDK's chat.completions.create() rejects unknown - # top-level kwargs. Route it through `extra_body` so it is merged - # into the payload instead of passed as a keyword argument. - extra_body: Final = optional_params.setdefault("extra_body", {}) - extra_body["thinking"] = thinking - - return optional_params + payload: Final[ThinkingPayload] = {"type": "disabled" if reasoning_effort == "none" else "enabled"} + return cls._coerce_thinking_type_for_model(model=model, thinking=payload) + return None @staticmethod - def _normalize_thinking_type_for_model(model: str, thinking: dict) -> dict: - """Coerce `thinking.type` values the model does not accept. + def _coerce_thinking_type_for_model(model: str, thinking: Mapping[str, object]) -> Mapping[str, object]: + """Coerce `thinking.type` to a value the model accepts. - MiniMax models on TokenHub only accept "adaptive" or "disabled" — - sending "enabled" returns a 400. "adaptive" is the closest semantic - (the model decides when to think), so "enabled" is coerced to it. + MiniMax models on TokenHub only accept "adaptive"/"disabled" and reject + "enabled" with a 400; "adaptive" (the model decides when to think) is + the closest semantic, so "enabled" is coerced for them. The capability + is read from the model map's `supports_adaptive_thinking` flag, so + aliases and newly onboarded adaptive-only models need no code change. Ref: https://www.tencentcloud.com/document/product/1300/82345 """ - if thinking.get("type") == "enabled" and "minimax" in model.lower(): - return {**thinking, "type": "adaptive"} - return thinking + if thinking.get("type") != "enabled" or not TencentChatConfig._is_adaptive_thinking_model(model): + return thinking + + budget: Final = thinking.get("budget_tokens") + if isinstance(budget, int): + coerced_with_budget: Final[ThinkingPayload] = {"type": "adaptive", "budget_tokens": budget} + return coerced_with_budget + coerced: Final[ThinkingPayload] = {"type": "adaptive"} + return coerced + + @staticmethod + def _is_adaptive_thinking_model(model: str) -> bool: + """Read `supports_adaptive_thinking` from the model map under tencent.""" + try: + model_info: Final = litellm.get_model_info(model=model, custom_llm_provider="tencent") + except Exception: # noqa: BLE001 # get_model_info raises a bare Exception for unmapped models + return False + return model_info.get("supports_adaptive_thinking") is True def _get_openai_compatible_provider_info( self, api_base: str | None, api_key: str | None diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 3af7d9e5019..884507a8905 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -49764,6 +49764,26 @@ "supports_reasoning": true, "supports_vision": false }, + "tencent/minimax-m3": { + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 6e-08, + "input_cost_per_token": 3e-07, + "input_cost_per_token_cache_hit": 6e-08, + "litellm_provider": "tencent", + "max_input_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://www.tencentcloud.com/products/tokenhub", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supports_adaptive_thinking": true, + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_vision": false + }, "cognition/swe-1.6": { "input_cost_per_token": 5e-07, "output_cost_per_token": 2.5e-06, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 3af7d9e5019..884507a8905 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -49764,6 +49764,26 @@ "supports_reasoning": true, "supports_vision": false }, + "tencent/minimax-m3": { + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 6e-08, + "input_cost_per_token": 3e-07, + "input_cost_per_token_cache_hit": 6e-08, + "litellm_provider": "tencent", + "max_input_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://www.tencentcloud.com/products/tokenhub", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supports_adaptive_thinking": true, + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_vision": false + }, "cognition/swe-1.6": { "input_cost_per_token": 5e-07, "output_cost_per_token": 2.5e-06, diff --git a/tests/test_litellm/llms/tencent/chat/test_tencent_chat_transformation.py b/tests/test_litellm/llms/tencent/chat/test_tencent_chat_transformation.py index 806d585a4c9..a540ea6cacd 100644 --- a/tests/test_litellm/llms/tencent/chat/test_tencent_chat_transformation.py +++ b/tests/test_litellm/llms/tencent/chat/test_tencent_chat_transformation.py @@ -159,17 +159,22 @@ def test_transform_request_never_passes_thinking_as_top_level_kwarg(): assert data["extra_body"]["thinking"] == {"type": "enabled", "budget_tokens": 1024} -class TestMinimaxThinkingCoercion: +class TestAdaptiveThinkingCoercion: """ - MiniMax models on TokenHub only accept thinking.type "adaptive"/"disabled" — - "enabled" returns a 400. Ref: https://www.tencentcloud.com/document/product/1300/82345 + Models flagged `supports_adaptive_thinking` in the cost map (e.g. + tencent/minimax-m3) only accept thinking.type "adaptive"/"disabled" — + "enabled" returns a 400 from TokenHub. + Ref: https://www.tencentcloud.com/document/product/1300/82345 """ - def test_reasoning_effort_maps_to_adaptive_for_minimax(self): + def test_reasoning_effort_maps_to_adaptive_for_adaptive_only_model(self): config = TencentChatConfig() - with patch( - "litellm.llms.tencent.chat.transformation.supports_reasoning", - return_value=True, + with ( + patch( + "litellm.llms.tencent.chat.transformation.supports_reasoning", + return_value=True, + ), + patch.object(TencentChatConfig, "_is_adaptive_thinking_model", return_value=True), ): result = config.map_openai_params( non_default_params={"reasoning_effort": "medium"}, @@ -180,26 +185,32 @@ class TestMinimaxThinkingCoercion: assert result["extra_body"]["thinking"] == {"type": "adaptive"} - def test_explicit_enabled_thinking_coerced_to_adaptive_for_minimax(self): + def test_explicit_enabled_thinking_coerced_to_adaptive(self): config = TencentChatConfig() - with patch( - "litellm.llms.tencent.chat.transformation.supports_reasoning", - return_value=True, + with ( + patch( + "litellm.llms.tencent.chat.transformation.supports_reasoning", + return_value=True, + ), + patch.object(TencentChatConfig, "_is_adaptive_thinking_model", return_value=True), ): result = config.map_openai_params( non_default_params={"thinking": {"type": "enabled", "budget_tokens": 4096}}, optional_params={}, - model="minimax-m3", + model="tencent/minimax-m3", drop_params=False, ) assert result["extra_body"]["thinking"] == {"type": "adaptive", "budget_tokens": 4096} - def test_disabled_thinking_kept_for_minimax(self): + def test_disabled_thinking_kept_for_adaptive_only_model(self): config = TencentChatConfig() - with patch( - "litellm.llms.tencent.chat.transformation.supports_reasoning", - return_value=True, + with ( + patch( + "litellm.llms.tencent.chat.transformation.supports_reasoning", + return_value=True, + ), + patch.object(TencentChatConfig, "_is_adaptive_thinking_model", return_value=True), ): result = config.map_openai_params( non_default_params={"thinking": {"type": "disabled"}}, @@ -210,11 +221,14 @@ class TestMinimaxThinkingCoercion: assert result["extra_body"]["thinking"] == {"type": "disabled"} - def test_none_reasoning_effort_disables_thinking_for_minimax(self): + def test_none_reasoning_effort_disables_thinking_for_adaptive_only_model(self): config = TencentChatConfig() - with patch( - "litellm.llms.tencent.chat.transformation.supports_reasoning", - return_value=True, + with ( + patch( + "litellm.llms.tencent.chat.transformation.supports_reasoning", + return_value=True, + ), + patch.object(TencentChatConfig, "_is_adaptive_thinking_model", return_value=True), ): result = config.map_openai_params( non_default_params={"reasoning_effort": "none"}, @@ -225,11 +239,14 @@ class TestMinimaxThinkingCoercion: assert result["extra_body"]["thinking"] == {"type": "disabled"} - def test_non_minimax_model_keeps_enabled(self): + def test_non_adaptive_model_keeps_enabled(self): config = TencentChatConfig() - with patch( - "litellm.llms.tencent.chat.transformation.supports_reasoning", - return_value=True, + with ( + patch( + "litellm.llms.tencent.chat.transformation.supports_reasoning", + return_value=True, + ), + patch.object(TencentChatConfig, "_is_adaptive_thinking_model", return_value=False), ): result = config.map_openai_params( non_default_params={"reasoning_effort": "high"}, @@ -240,6 +257,28 @@ class TestMinimaxThinkingCoercion: assert result["extra_body"]["thinking"] == {"type": "enabled"} + def test_unmapped_model_keeps_enabled(self): + """Models absent from the cost map never get coerced.""" + config = TencentChatConfig() + assert config._is_adaptive_thinking_model("tencent/no-such-model") is False + + +def test_minimax_m3_cost_map_entry_marks_adaptive_thinking(): + """The capability flag driving the coercion must exist in the cost map + (and its backup, which is shipped with the package).""" + import json + from pathlib import Path + + repo_root = Path(__file__).parents[5] + for filename in ("model_prices_and_context_window.json", "litellm/model_prices_and_context_window_backup.json"): + with open(repo_root / filename) as f: + entry = json.load(f).get("tencent/minimax-m3") + + assert entry is not None, f"tencent/minimax-m3 not found in {filename}" + assert entry["litellm_provider"] == "tencent" + assert entry.get("supports_adaptive_thinking") is True + assert entry.get("supports_reasoning") is True + def test_get_complete_url_default(): config = TencentChatConfig() From 1065856548cc8dc70860546c1daedfff95b133c8 Mon Sep 17 00:00:00 2001 From: Felipe Rodrigues Gare Carnielli Date: Mon, 24 Aug 2026 18:41:50 -0300 Subject: [PATCH 4/5] fix(tencent): satisfy basedpyright budget in thinking mapping Suppress the three reportUnknownArgumentType diagnostics with reasons at the untyped provider-params boundary, collapse the early return, and assign extra_body via a TypedDict-annotated literal so the file's basedpyright profile matches the merge base exactly. The user-supplied extra_body merge is covered end-to-end through get_optional_params. --- litellm/llms/tencent/chat/transformation.py | 34 ++++++++----------- .../chat/test_tencent_chat_transformation.py | 27 +++++++++++++-- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/litellm/llms/tencent/chat/transformation.py b/litellm/llms/tencent/chat/transformation.py index 283a227943d..7e80b0012df 100644 --- a/litellm/llms/tencent/chat/transformation.py +++ b/litellm/llms/tencent/chat/transformation.py @@ -27,8 +27,8 @@ class ThinkingPayload(TypedDict, total=False): budget_tokens: ReadOnly[int] -class TencentExtraBody(TypedDict, total=False): - """`extra_body` payload for TokenHub chat requests.""" +class ThinkingExtraBody(TypedDict, total=False): + """`extra_body` payload carrying TokenHub's `thinking` object.""" thinking: ReadOnly[Mapping[str, object]] @@ -54,21 +54,17 @@ class TencentChatConfig(OpenAIGPTConfig): thinking: Final = self._resolve_thinking_payload( model=model, - thinking_value=thinking_value, - reasoning_effort=reasoning_effort, + thinking_value=thinking_value, # pyright: ignore[reportUnknownArgumentType] # value popped from the untyped provider params dict + reasoning_effort=reasoning_effort, # pyright: ignore[reportUnknownArgumentType] # value popped from the untyped provider params dict ) - if thinking is None: - return mapped_params - - # TokenHub expects `thinking` in the request JSON body, but the OpenAI - # SDK's chat.completions.create() rejects unknown top-level kwargs, so - # it travels via `extra_body`, which the SDK merges into the payload. - existing_extra_body: Final = mapped_params.pop("extra_body", None) - if isinstance(existing_extra_body, dict): - merged_extra_body: Final[TencentExtraBody] = {**existing_extra_body, "thinking": thinking} - else: - merged_extra_body: Final[TencentExtraBody] = {"thinking": thinking} - mapped_params["extra_body"] = merged_extra_body + if thinking is not None: + # TokenHub expects `thinking` in the request JSON body, but the + # OpenAI SDK's chat.completions.create() rejects unknown top-level + # kwargs, so it travels via `extra_body`, which the SDK merges into + # the payload. A plain assignment is merge-safe: get_optional_params + # spreads this dict into its own extra_body assembly downstream. + extra_body: Final[ThinkingExtraBody] = {"thinking": thinking} + mapped_params["extra_body"] = extra_body return mapped_params @classmethod @@ -79,7 +75,7 @@ class TencentChatConfig(OpenAIGPTConfig): reasoning_effort: object, ) -> Mapping[str, object] | None: if isinstance(thinking_value, dict): - return cls._coerce_thinking_type_for_model(model=model, thinking=thinking_value) + return cls._coerce_thinking_type_for_model(model=model, thinking=thinking_value) # pyright: ignore[reportUnknownArgumentType] # isinstance narrows to dict[Unknown, Unknown] out of the untyped provider params dict if isinstance(reasoning_effort, str): # TokenHub recommends explicitly disabling thinking rather than # relying on per-model defaults (deepseek-v4-* default to enabled). @@ -101,7 +97,7 @@ class TencentChatConfig(OpenAIGPTConfig): if thinking.get("type") != "enabled" or not TencentChatConfig._is_adaptive_thinking_model(model): return thinking - budget: Final = thinking.get("budget_tokens") + budget: Final[object] = thinking.get("budget_tokens") if isinstance(budget, int): coerced_with_budget: Final[ThinkingPayload] = {"type": "adaptive", "budget_tokens": budget} return coerced_with_budget @@ -112,7 +108,7 @@ class TencentChatConfig(OpenAIGPTConfig): def _is_adaptive_thinking_model(model: str) -> bool: """Read `supports_adaptive_thinking` from the model map under tencent.""" try: - model_info: Final = litellm.get_model_info(model=model, custom_llm_provider="tencent") + model_info: Final[Mapping[str, object]] = litellm.get_model_info(model=model, custom_llm_provider="tencent") except Exception: # noqa: BLE001 # get_model_info raises a bare Exception for unmapped models return False return model_info.get("supports_adaptive_thinking") is True diff --git a/tests/test_litellm/llms/tencent/chat/test_tencent_chat_transformation.py b/tests/test_litellm/llms/tencent/chat/test_tencent_chat_transformation.py index a540ea6cacd..e8f5db09c4b 100644 --- a/tests/test_litellm/llms/tencent/chat/test_tencent_chat_transformation.py +++ b/tests/test_litellm/llms/tencent/chat/test_tencent_chat_transformation.py @@ -118,7 +118,9 @@ def test_map_openai_params_extracts_thinking_and_effort_from_optional_params(): assert "reasoning_effort" not in result -def test_map_openai_params_merges_into_existing_extra_body(): +def test_map_openai_params_overwrites_existing_extra_body(): + """The map layer assigns extra_body directly; get_optional_params merges it + with user-supplied extra params downstream (utils.py provider overrides).""" config = TencentChatConfig() result = config.map_openai_params( non_default_params={}, @@ -130,7 +132,28 @@ def test_map_openai_params_merges_into_existing_extra_body(): drop_params=False, ) - assert result["extra_body"] == {"custom_flag": True, "thinking": {"type": "enabled"}} + assert result["extra_body"] == {"thinking": {"type": "enabled"}} + + +def test_get_optional_params_merges_thinking_with_user_extra_body(): + """End-to-end at the get_optional_params layer: a user-supplied extra_body + and the mapped thinking payload must coexist in the final extra_body.""" + from litellm.utils import get_optional_params + + with patch( + "litellm.llms.tencent.chat.transformation.supports_reasoning", + return_value=True, + ): + result = get_optional_params( + model="tencent/deepseek-v4-pro", + custom_llm_provider="tencent", + messages=[{"role": "user", "content": "hi"}], + thinking={"type": "enabled"}, + extra_body={"custom_flag": True}, + ) + + assert result["extra_body"]["thinking"] == {"type": "enabled"} + assert result["extra_body"]["custom_flag"] is True def test_transform_request_never_passes_thinking_as_top_level_kwarg(): From 9c38f6f1257c58905f6f7abf3599184611206c7b Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:34:06 -0700 Subject: [PATCH 5/5] test(tencent): drive thinking tests off the real cost map instead of patched internals --- .../chat/test_tencent_chat_transformation.py | 125 ++++++------------ 1 file changed, 43 insertions(+), 82 deletions(-) diff --git a/tests/test_litellm/llms/tencent/chat/test_tencent_chat_transformation.py b/tests/test_litellm/llms/tencent/chat/test_tencent_chat_transformation.py index e8f5db09c4b..9f510786d50 100644 --- a/tests/test_litellm/llms/tencent/chat/test_tencent_chat_transformation.py +++ b/tests/test_litellm/llms/tencent/chat/test_tencent_chat_transformation.py @@ -135,22 +135,18 @@ def test_map_openai_params_overwrites_existing_extra_body(): assert result["extra_body"] == {"thinking": {"type": "enabled"}} -def test_get_optional_params_merges_thinking_with_user_extra_body(): +def test_get_optional_params_merges_thinking_with_user_extra_body(local_model_cost_map): """End-to-end at the get_optional_params layer: a user-supplied extra_body and the mapped thinking payload must coexist in the final extra_body.""" from litellm.utils import get_optional_params - with patch( - "litellm.llms.tencent.chat.transformation.supports_reasoning", - return_value=True, - ): - result = get_optional_params( - model="tencent/deepseek-v4-pro", - custom_llm_provider="tencent", - messages=[{"role": "user", "content": "hi"}], - thinking={"type": "enabled"}, - extra_body={"custom_flag": True}, - ) + result = get_optional_params( + model="tencent/deepseek-v4-pro", + custom_llm_provider="tencent", + messages=[{"role": "user", "content": "hi"}], + thinking={"type": "enabled"}, + extra_body={"custom_flag": True}, + ) assert result["extra_body"]["thinking"] == {"type": "enabled"} assert result["extra_body"]["custom_flag"] is True @@ -190,93 +186,58 @@ class TestAdaptiveThinkingCoercion: Ref: https://www.tencentcloud.com/document/product/1300/82345 """ - def test_reasoning_effort_maps_to_adaptive_for_adaptive_only_model(self): + def test_reasoning_effort_maps_to_adaptive_for_adaptive_only_model(self, local_model_cost_map): config = TencentChatConfig() - with ( - patch( - "litellm.llms.tencent.chat.transformation.supports_reasoning", - return_value=True, - ), - patch.object(TencentChatConfig, "_is_adaptive_thinking_model", return_value=True), - ): - result = config.map_openai_params( - non_default_params={"reasoning_effort": "medium"}, - optional_params={}, - model="tencent/minimax-m3", - drop_params=False, - ) + result = config.map_openai_params( + non_default_params={"reasoning_effort": "medium"}, + optional_params={}, + model="tencent/minimax-m3", + drop_params=False, + ) assert result["extra_body"]["thinking"] == {"type": "adaptive"} - def test_explicit_enabled_thinking_coerced_to_adaptive(self): + def test_explicit_enabled_thinking_coerced_to_adaptive(self, local_model_cost_map): config = TencentChatConfig() - with ( - patch( - "litellm.llms.tencent.chat.transformation.supports_reasoning", - return_value=True, - ), - patch.object(TencentChatConfig, "_is_adaptive_thinking_model", return_value=True), - ): - result = config.map_openai_params( - non_default_params={"thinking": {"type": "enabled", "budget_tokens": 4096}}, - optional_params={}, - model="tencent/minimax-m3", - drop_params=False, - ) + result = config.map_openai_params( + non_default_params={"thinking": {"type": "enabled", "budget_tokens": 4096}}, + optional_params={}, + model="tencent/minimax-m3", + drop_params=False, + ) assert result["extra_body"]["thinking"] == {"type": "adaptive", "budget_tokens": 4096} - def test_disabled_thinking_kept_for_adaptive_only_model(self): + def test_disabled_thinking_kept_for_adaptive_only_model(self, local_model_cost_map): config = TencentChatConfig() - with ( - patch( - "litellm.llms.tencent.chat.transformation.supports_reasoning", - return_value=True, - ), - patch.object(TencentChatConfig, "_is_adaptive_thinking_model", return_value=True), - ): - result = config.map_openai_params( - non_default_params={"thinking": {"type": "disabled"}}, - optional_params={}, - model="tencent/minimax-m3", - drop_params=False, - ) + result = config.map_openai_params( + non_default_params={"thinking": {"type": "disabled"}}, + optional_params={}, + model="tencent/minimax-m3", + drop_params=False, + ) assert result["extra_body"]["thinking"] == {"type": "disabled"} - def test_none_reasoning_effort_disables_thinking_for_adaptive_only_model(self): + def test_none_reasoning_effort_disables_thinking_for_adaptive_only_model(self, local_model_cost_map): config = TencentChatConfig() - with ( - patch( - "litellm.llms.tencent.chat.transformation.supports_reasoning", - return_value=True, - ), - patch.object(TencentChatConfig, "_is_adaptive_thinking_model", return_value=True), - ): - result = config.map_openai_params( - non_default_params={"reasoning_effort": "none"}, - optional_params={}, - model="tencent/minimax-m3", - drop_params=False, - ) + result = config.map_openai_params( + non_default_params={"reasoning_effort": "none"}, + optional_params={}, + model="tencent/minimax-m3", + drop_params=False, + ) assert result["extra_body"]["thinking"] == {"type": "disabled"} - def test_non_adaptive_model_keeps_enabled(self): + def test_non_adaptive_model_keeps_enabled(self, local_model_cost_map): config = TencentChatConfig() - with ( - patch( - "litellm.llms.tencent.chat.transformation.supports_reasoning", - return_value=True, - ), - patch.object(TencentChatConfig, "_is_adaptive_thinking_model", return_value=False), - ): - result = config.map_openai_params( - non_default_params={"reasoning_effort": "high"}, - optional_params={}, - model="tencent/kimi-k3", - drop_params=False, - ) + result = config.map_openai_params( + non_default_params={"reasoning_effort": "high"}, + optional_params={}, + model="tencent/deepseek-v4-pro", + drop_params=False, + ) assert result["extra_body"]["thinking"] == {"type": "enabled"}