diff --git a/litellm/__init__.py b/litellm/__init__.py index 9327e121b1d..2ec0830d622 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -588,6 +588,7 @@ gemini_models: Set = set() xai_models: Set = set() zai_models: Set = set() deepseek_models: Set = set() +tencent_models: Set = set() runwayml_models: Set = set() azure_ai_models: Set = set() jina_ai_models: Set = set() @@ -801,6 +802,8 @@ def add_known_models(model_cost_map: Optional[Dict] = None): fal_ai_models.add(key) elif value.get("litellm_provider") == "deepseek": deepseek_models.add(key) + elif value.get("litellm_provider") == "tencent": + tencent_models.add(key) elif value.get("litellm_provider") == "runwayml": runwayml_models.add(key) elif value.get("litellm_provider") == "meta_llama": @@ -1093,6 +1096,7 @@ models_by_provider: dict = { "zai": zai_models, "fal_ai": fal_ai_models, "deepseek": deepseek_models, + "tencent": tencent_models, "runwayml": runwayml_models, "mistral": mistral_chat_models, "azure_ai": azure_ai_models, @@ -1804,6 +1808,9 @@ if TYPE_CHECKING: from .llms.deepseek.chat.transformation import ( DeepSeekChatConfig as _DeepSeekChatConfig, ) + from .llms.tencent.chat.transformation import ( + TencentChatConfig as _TencentChatConfig, + ) from .llms.sap.chat.transformation import ( GenAIHubOrchestrationConfig as _GenAIHubOrchestrationConfig, ) @@ -1846,6 +1853,7 @@ if TYPE_CHECKING: # Type stubs for lazy-loaded config classes (to help mypy understand types) VLLMConfig: Type[_VLLMConfig] DeepSeekChatConfig: Type[_DeepSeekChatConfig] + TencentChatConfig: Type[_TencentChatConfig] GenAIHubOrchestrationConfig: Type[_GenAIHubOrchestrationConfig] GenAIHubEmbeddingConfig: Type[_GenAIHubEmbeddingConfig] AzureOpenAIO1Config: Type[_AzureOpenAIO1Config] diff --git a/litellm/_lazy_imports_registry.py b/litellm/_lazy_imports_registry.py index 0f9d3a560d1..488331e3895 100644 --- a/litellm/_lazy_imports_registry.py +++ b/litellm/_lazy_imports_registry.py @@ -284,6 +284,7 @@ LLM_CONFIG_NAMES = ( "LiteLLMProxyChatConfig", "VLLMConfig", "DeepSeekChatConfig", + "TencentChatConfig", "LMStudioChatConfig", "LmStudioEmbeddingConfig", "NscaleConfig", @@ -1096,6 +1097,7 @@ _LLM_CONFIGS_IMPORT_MAP = { ), "VLLMConfig": (".llms.vllm.completion.transformation", "VLLMConfig"), "DeepSeekChatConfig": (".llms.deepseek.chat.transformation", "DeepSeekChatConfig"), + "TencentChatConfig": (".llms.tencent.chat.transformation", "TencentChatConfig"), "LMStudioChatConfig": (".llms.lm_studio.chat.transformation", "LMStudioChatConfig"), "LmStudioEmbeddingConfig": ( ".llms.lm_studio.embed.transformation", diff --git a/litellm/constants.py b/litellm/constants.py index 6eb2779dcae..1300668cc70 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -508,6 +508,7 @@ LITELLM_CHAT_PROVIDERS = [ "text-completion-codestral", "text-completion-inception", "deepseek", + "tencent", "sambanova", "maritalk", "cloudflare", @@ -729,6 +730,7 @@ openai_compatible_providers: List = [ "volcengine", "codestral", "deepseek", + "tencent", "deepinfra", "perplexity", "xinference", diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index e8535a570c8..8f4dd5badce 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -52,6 +52,9 @@ from litellm.llms.databricks.cost_calculator import ( from litellm.llms.deepseek.cost_calculator import ( cost_per_token as deepseek_cost_per_token, ) +from litellm.llms.tencent.cost_calculator import ( + cost_per_token as tencent_cost_per_token, +) from litellm.llms.fireworks_ai.cost_calculator import ( cost_per_token as fireworks_ai_cost_per_token, ) @@ -625,6 +628,8 @@ def cost_per_token( return gemini_cost_per_token(model=model, usage=usage_block, service_tier=service_tier) elif custom_llm_provider == "deepseek": return deepseek_cost_per_token(model=model, usage=usage_block) + elif custom_llm_provider == "tencent": + return tencent_cost_per_token(model=model, usage=usage_block) elif custom_llm_provider == "perplexity": return perplexity_cost_per_token(model=model, usage=usage_block) elif custom_llm_provider == "xai": diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index a7a576ff167..61a73201c43 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -652,6 +652,10 @@ def _get_openai_compatible_provider_info( api_base = api_base or get_secret("DEEPSEEK_API_BASE") or "https://api.deepseek.com/beta" # type: ignore dynamic_api_key = api_key or get_secret_str("DEEPSEEK_API_KEY") + elif custom_llm_provider == "tencent": + api_base = api_base or get_secret("TENCENT_API_BASE") or "https://tokenhub-intl.tencentcloudmaas.com/v1" + + dynamic_api_key = api_key or get_secret_str("TENCENT_API_KEY") elif custom_llm_provider == "fireworks_ai": # fireworks is openai compatible, we just need to set this to custom_openai and have the api_base be https://api.fireworks.ai/inference/v1 ( diff --git a/litellm/litellm_core_utils/get_supported_openai_params.py b/litellm/litellm_core_utils/get_supported_openai_params.py index c4ddb4b7ee0..19149da0316 100644 --- a/litellm/litellm_core_utils/get_supported_openai_params.py +++ b/litellm/litellm_core_utils/get_supported_openai_params.py @@ -106,6 +106,8 @@ def get_supported_openai_params( return litellm.VLLMConfig().get_supported_openai_params(model=model) elif custom_llm_provider == "deepseek": return litellm.DeepSeekChatConfig().get_supported_openai_params(model=model) + elif custom_llm_provider == "tencent": + return litellm.TencentChatConfig().get_supported_openai_params(model=model) elif custom_llm_provider == "cohere_chat" or custom_llm_provider == "cohere": return litellm.CohereChatConfig().get_supported_openai_params(model=model) elif custom_llm_provider == "maritalk": diff --git a/litellm/llms/tencent/__init__.py b/litellm/llms/tencent/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/litellm/llms/tencent/chat/__init__.py b/litellm/llms/tencent/chat/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/litellm/llms/tencent/chat/transformation.py b/litellm/llms/tencent/chat/transformation.py new file mode 100644 index 00000000000..4dea0c4b8c7 --- /dev/null +++ b/litellm/llms/tencent/chat/transformation.py @@ -0,0 +1,68 @@ +""" +Translates from OpenAI's `/v1/chat/completions` to Tencent TokenHub's +OpenAI-compatible endpoint. +""" + +from typing import Optional + +from litellm.secret_managers.main import get_secret_str +from litellm.utils import supports_reasoning + +from ...openai.chat.gpt_transformation import OpenAIGPTConfig + + +class TencentChatConfig(OpenAIGPTConfig): + def get_supported_openai_params(self, model: str) -> list: + params = super().get_supported_openai_params(model) + if supports_reasoning(model, custom_llm_provider="tencent"): + params.extend(["thinking", "reasoning_effort"]) + return params + + def map_openai_params( + self, + non_default_params: dict, + optional_params: dict, + model: str, + drop_params: bool, + ) -> dict: + optional_params = super().map_openai_params(non_default_params, optional_params, model, drop_params) + + thinking_value = optional_params.pop("thinking", None) + reasoning_effort = 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"} + + return optional_params + + def _get_openai_compatible_provider_info( + self, api_base: Optional[str], api_key: Optional[str] + ) -> tuple[Optional[str], Optional[str]]: + api_base = api_base or get_secret_str("TENCENT_API_BASE") or "https://tokenhub-intl.tencentcloudmaas.com/v1" + dynamic_api_key = api_key or get_secret_str("TENCENT_API_KEY") + return api_base, dynamic_api_key + + def get_complete_url( + self, + api_base: Optional[str], + api_key: Optional[str], + model: str, + optional_params: dict, + litellm_params: dict, + stream: Optional[bool] = None, + ) -> str: + if not api_base: + api_base = "https://tokenhub-intl.tencentcloudmaas.com/v1" + + api_base = api_base.rstrip("/") + + if api_base.endswith("/chat/completions"): + return api_base + + if not api_base.endswith("/v1"): + api_base = f"{api_base}/v1" + + return f"{api_base}/chat/completions" diff --git a/litellm/llms/tencent/cost_calculator.py b/litellm/llms/tencent/cost_calculator.py new file mode 100644 index 00000000000..d9aebdc3284 --- /dev/null +++ b/litellm/llms/tencent/cost_calculator.py @@ -0,0 +1,6 @@ +from litellm.litellm_core_utils.llm_cost_calc.utils import generic_cost_per_token +from litellm.types.utils import Usage + + +def cost_per_token(model: str, usage: Usage) -> tuple[float, float]: + return generic_cost_per_token(model=model, usage=usage, custom_llm_provider="tencent") diff --git a/litellm/llms/tencent/messages/__init__.py b/litellm/llms/tencent/messages/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/litellm/llms/tencent/messages/transformation.py b/litellm/llms/tencent/messages/transformation.py new file mode 100644 index 00000000000..e0f13aa9ca4 --- /dev/null +++ b/litellm/llms/tencent/messages/transformation.py @@ -0,0 +1,85 @@ +""" +Tencent Anthropic-compatible messages transformation config. + +Tencent TokenHub exposes an Anthropic-compatible Messages API endpoint +alongside its standard OpenAI-compatible chat completions endpoint. +""" + +from typing import Any, Optional + +import litellm +from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( + AnthropicMessagesConfig, +) +from litellm.secret_managers.main import get_secret_str + + +class TencentAnthropicMessagesConfig(AnthropicMessagesConfig): + """ + Tencent TokenHub exposes an Anthropic-compatible Messages API. + + Unlike the chat completions endpoint (which uses /v1), the Anthropic + endpoint may use a different base URL. Configure via + TENCENT_ANTHROPIC_API_BASE or TENCENT_API_BASE. + """ + + @property + def custom_llm_provider(self) -> Optional[str]: + return "tencent" + + def should_strip_billing_metadata(self) -> bool: + return True + + @staticmethod + def get_api_key(api_key: Optional[str] = None) -> Optional[str]: + return api_key or get_secret_str("TENCENT_API_KEY") or litellm.api_key + + @staticmethod + def get_api_base(api_base: Optional[str] = None) -> str: + return ( + api_base + or get_secret_str("TENCENT_ANTHROPIC_API_BASE") + or get_secret_str("TENCENT_API_BASE") + or "https://tokenhub-intl.tencentcloudmaas.com" + ) + + def validate_anthropic_messages_environment( + self, + headers: dict, + model: str, + messages: list[Any], + optional_params: dict, + litellm_params: dict, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + ) -> tuple[dict, Optional[str]]: + return super().validate_anthropic_messages_environment( + headers=headers, + model=model, + messages=messages, + optional_params=optional_params, + litellm_params=litellm_params, + api_key=self.get_api_key(api_key=api_key), + api_base=api_base, + ) + + def get_complete_url( + self, + api_base: Optional[str], + api_key: Optional[str], + model: str, + optional_params: dict, + litellm_params: dict, + stream: Optional[bool] = None, + ) -> str: + base_url = self.get_api_base(api_base=api_base).rstrip("/") + + if base_url.endswith("/v1/messages"): + return base_url + + if base_url.endswith("/v1/chat/completions"): + base_url = base_url[: -len("/v1/chat/completions")] + elif base_url.endswith("/v1"): + base_url = base_url[: -len("/v1")] + + return f"{base_url}/v1/messages" diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index a4cca8d6cb0..26d54c05ffb 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -43675,6 +43675,58 @@ "supports_tool_choice": true, "supports_vision": false }, + "tencent/deepseek-v4-pro": { + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 3.625e-09, + "input_cost_per_token": 4.35e-07, + "input_cost_per_token_cache_hit": 3.625e-09, + "litellm_provider": "tencent", + "max_input_tokens": 1000000, + "max_output_tokens": 384000, + "max_tokens": 384000, + "mode": "chat", + "output_cost_per_token": 8.7e-07, + "source": "https://www.tencentcloud.com/products/tokenhub", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "supports_vision": false + }, + "tencent/deepseek-v4-flash": { + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 2.8e-09, + "input_cost_per_token": 1.4e-07, + "input_cost_per_token_cache_hit": 2.8e-09, + "litellm_provider": "tencent", + "max_input_tokens": 1000000, + "max_output_tokens": 384000, + "max_tokens": 384000, + "mode": "chat", + "output_cost_per_token": 2.8e-07, + "source": "https://www.tencentcloud.com/products/tokenhub", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "supports_vision": false + }, "pinstripes/ps/glm-4.5-air": { "max_tokens": 128000, "max_input_tokens": 128000, diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 997498803ac..2e0eceaddd9 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3311,6 +3311,7 @@ class LlmProviders(str, Enum): CUSTOM = "custom" LITELLM_PROXY = "litellm_proxy" HOSTED_VLLM = "hosted_vllm" + TENCENT = "tencent" LLAMAFILE = "llamafile" LM_STUDIO = "lm_studio" GALADRIEL = "galadriel" diff --git a/litellm/utils.py b/litellm/utils.py index b5f77d75b93..83d129339a4 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -4204,6 +4204,13 @@ def get_optional_params( model=model, drop_params=(drop_params if drop_params is not None and isinstance(drop_params, bool) else False), ) + elif custom_llm_provider == "tencent": + optional_params = litellm.TencentChatConfig().map_openai_params( + non_default_params=non_default_params, + optional_params=optional_params, + model=model, + drop_params=(drop_params if drop_params is not None and isinstance(drop_params, bool) else False), + ) elif custom_llm_provider == "openrouter": optional_params = litellm.OpenrouterConfig().map_openai_params( non_default_params=non_default_params, @@ -6017,6 +6024,11 @@ def validate_environment( keys_in_environment = True else: missing_keys.append("DEEPSEEK_API_KEY") + elif custom_llm_provider == "tencent": + if "TENCENT_API_KEY" in os.environ: + keys_in_environment = True + else: + missing_keys.append("TENCENT_API_KEY") elif custom_llm_provider == "mistral": if "MISTRAL_API_KEY" in os.environ: keys_in_environment = True @@ -7558,6 +7570,7 @@ class ProviderConfigManager: ), # Simple provider mappings (no model parameter needed) LlmProviders.DEEPSEEK: (lambda: litellm.DeepSeekChatConfig(), False), + LlmProviders.TENCENT: (lambda: litellm.TencentChatConfig(), False), LlmProviders.GROQ: (lambda: litellm.GroqChatConfig(), False), LlmProviders.BEDROCK_MANTLE: ( lambda: litellm.BedrockMantleChatConfig(), @@ -7996,6 +8009,12 @@ class ProviderConfigManager: ) return DeepSeekAnthropicMessagesConfig() + elif litellm.LlmProviders.TENCENT == provider: + from litellm.llms.tencent.messages.transformation import ( + TencentAnthropicMessagesConfig, + ) + + return TencentAnthropicMessagesConfig() elif litellm.LlmProviders.GITHUB_COPILOT == provider: if "claude" in model_lower: from litellm.llms.github_copilot.messages.transformation import ( diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index e85d5eeef4b..a3dcdaed3f7 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -43874,6 +43874,58 @@ "supports_tool_choice": true, "supports_vision": false }, + "tencent/deepseek-v4-pro": { + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 3.625e-09, + "input_cost_per_token": 4.35e-07, + "input_cost_per_token_cache_hit": 3.625e-09, + "litellm_provider": "tencent", + "max_input_tokens": 1000000, + "max_output_tokens": 384000, + "max_tokens": 384000, + "mode": "chat", + "output_cost_per_token": 8.7e-07, + "source": "https://www.tencentcloud.com/products/tokenhub", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "supports_vision": false + }, + "tencent/deepseek-v4-flash": { + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 2.8e-09, + "input_cost_per_token": 1.4e-07, + "input_cost_per_token_cache_hit": 2.8e-09, + "litellm_provider": "tencent", + "max_input_tokens": 1000000, + "max_output_tokens": 384000, + "max_tokens": 384000, + "mode": "chat", + "output_cost_per_token": 2.8e-07, + "source": "https://www.tencentcloud.com/products/tokenhub", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supports_assistant_prefill": true, + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_parallel_function_calling": true, + "supports_prompt_caching": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "supports_vision": false + }, "pinstripes/ps/glm-4.5-air": { "max_tokens": 128000, "max_input_tokens": 128000, diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index edac8949f28..3034ada56ba 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -2295,6 +2295,24 @@ "text_completion": true } }, + "tencent": { + "display_name": "Tencent TokenHub (`tencent`)", + "url": "https://docs.litellm.ai/docs/providers/tencent", + "endpoints": { + "chat_completions": true, + "messages": true, + "responses": true, + "embeddings": false, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false, + "a2a": false, + "text_completion": false + } + }, "text-completion-codestral": { "display_name": "Text Completion Codestral (`text-completion-codestral`)", "url": "https://docs.litellm.ai/docs/providers/codestral", diff --git a/tests/test_litellm/llms/tencent/__init__.py b/tests/test_litellm/llms/tencent/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_litellm/llms/tencent/chat/__init__.py b/tests/test_litellm/llms/tencent/chat/__init__.py new file mode 100644 index 00000000000..e69de29bb2d 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 new file mode 100644 index 00000000000..00a82041c20 --- /dev/null +++ b/tests/test_litellm/llms/tencent/chat/test_tencent_chat_transformation.py @@ -0,0 +1,207 @@ +from unittest.mock import patch + +from litellm.llms.tencent.chat.transformation import TencentChatConfig + + +def test_supported_openai_params_includes_thinking_and_reasoning_effort(): + config = TencentChatConfig() + + with patch( + "litellm.llms.tencent.chat.transformation.supports_reasoning", + return_value=True, + ): + params = config.get_supported_openai_params(model="tencent/deepseek-v4-pro") + + assert "thinking" in params + assert "reasoning_effort" in params + assert "stream" in params + assert "temperature" in params + + +def test_supported_openai_params_excludes_thinking_without_reasoning_support(): + config = TencentChatConfig() + + with patch( + "litellm.llms.tencent.chat.transformation.supports_reasoning", + return_value=False, + ): + params = config.get_supported_openai_params(model="tencent/non-reasoning-model") + + assert "thinking" not in params + assert "reasoning_effort" not in params + assert "stream" in params + + +def test_map_openai_params_passes_thinking_dict_through(): + 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": 1024}}, + optional_params={}, + model="tencent/deepseek-v4-pro", + drop_params=False, + ) + + assert result["thinking"] == {"type": "enabled", "budget_tokens": 1024} + + +def test_map_openai_params_converts_reasoning_effort_to_thinking(): + 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/deepseek-v4-pro", + drop_params=False, + ) + + assert result["thinking"] == {"type": "enabled"} + + +def test_map_openai_params_drops_none_reasoning_effort(): + 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/deepseek-v4-pro", + drop_params=False, + ) + + assert "thinking" not in result + assert "reasoning_effort" not in result + + +def test_map_openai_params_thinking_priority_over_reasoning_effort(): + 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": 2048}, + "reasoning_effort": "high", + }, + optional_params={}, + model="tencent/deepseek-v4-pro", + drop_params=False, + ) + + assert result["thinking"] == {"type": "enabled", "budget_tokens": 2048} + + +def test_map_openai_params_extracts_thinking_and_effort_from_optional_params(): + config = TencentChatConfig() + result = config.map_openai_params( + non_default_params={}, + optional_params={"thinking": {"type": "enabled"}, "reasoning_effort": "medium"}, + model="tencent/deepseek-v4-pro", + drop_params=False, + ) + + assert "thinking" in result + assert "reasoning_effort" not in result + + +def test_get_complete_url_default(): + config = TencentChatConfig() + + url = config.get_complete_url( + api_base=None, + api_key=None, + model="tencent/deepseek-v4-pro", + optional_params={}, + litellm_params={}, + ) + + assert url == "https://tokenhub-intl.tencentcloudmaas.com/v1/chat/completions" + + +def test_get_complete_url_strips_trailing_slash(): + config = TencentChatConfig() + + url = config.get_complete_url( + api_base="https://tokenhub-intl.tencentcloudmaas.com/v1/", + api_key=None, + model="tencent/deepseek-v4-pro", + optional_params={}, + litellm_params={}, + ) + + assert url == "https://tokenhub-intl.tencentcloudmaas.com/v1/chat/completions" + + +def test_get_complete_url_custom_base_preserves_v1(): + config = TencentChatConfig() + + url = config.get_complete_url( + api_base="https://tokenhub.tencentcloudmaas.com/v1", + api_key=None, + model="tencent/deepseek-v4-pro", + optional_params={}, + litellm_params={}, + ) + + assert url == "https://tokenhub.tencentcloudmaas.com/v1/chat/completions" + + +def test_get_complete_url_adds_v1_to_custom_base(): + config = TencentChatConfig() + + url = config.get_complete_url( + api_base="https://tokenhub.tencentcloudmaas.com", + api_key=None, + model="tencent/deepseek-v4-pro", + optional_params={}, + litellm_params={}, + ) + + assert url == "https://tokenhub.tencentcloudmaas.com/v1/chat/completions" + + +def test_get_complete_url_does_not_append_to_full_url(): + config = TencentChatConfig() + + url = config.get_complete_url( + api_base="https://tokenhub.tencentcloudmaas.com/v1/chat/completions", + api_key=None, + model="tencent/deepseek-v4-pro", + optional_params={}, + litellm_params={}, + ) + + assert url == "https://tokenhub.tencentcloudmaas.com/v1/chat/completions" + + +def test_provider_info_falls_back_to_default_base(): + config = TencentChatConfig() + + with patch("litellm.llms.tencent.chat.transformation.get_secret_str", return_value=None): + api_base, api_key = config._get_openai_compatible_provider_info(api_base=None, api_key="sk-arg") + + assert api_base == "https://tokenhub-intl.tencentcloudmaas.com/v1" + assert api_key == "sk-arg" + + +def test_provider_info_reads_env_secrets(): + config = TencentChatConfig() + + secrets = {"TENCENT_API_BASE": "https://env.tencent/v1", "TENCENT_API_KEY": "sk-env"} + with patch( + "litellm.llms.tencent.chat.transformation.get_secret_str", + side_effect=lambda key: secrets.get(key), + ): + api_base, api_key = config._get_openai_compatible_provider_info(api_base=None, api_key=None) + + assert api_base == "https://env.tencent/v1" + assert api_key == "sk-env" diff --git a/tests/test_litellm/llms/tencent/messages/__init__.py b/tests/test_litellm/llms/tencent/messages/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_litellm/llms/tencent/messages/test_tencent_anthropic_messages_transformation.py b/tests/test_litellm/llms/tencent/messages/test_tencent_anthropic_messages_transformation.py new file mode 100644 index 00000000000..70c965a6190 --- /dev/null +++ b/tests/test_litellm/llms/tencent/messages/test_tencent_anthropic_messages_transformation.py @@ -0,0 +1,173 @@ +import litellm +from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( + AnthropicMessagesConfig, +) +from litellm.llms.tencent.messages.transformation import ( + TencentAnthropicMessagesConfig, +) +from litellm.utils import ProviderConfigManager + + +def test_tencent_provider_uses_anthropic_messages_config(): + config = ProviderConfigManager.get_provider_anthropic_messages_config( + model="deepseek-v4-pro", + provider=litellm.LlmProviders.TENCENT, + ) + + assert isinstance(config, TencentAnthropicMessagesConfig) + assert config.custom_llm_provider == "tencent" + + +def test_anthropic_provider_keeps_default_config_for_tencent_named_model(): + config = ProviderConfigManager.get_provider_anthropic_messages_config( + model="deepseek-v4-pro", + provider=litellm.LlmProviders.ANTHROPIC, + ) + + assert isinstance(config, AnthropicMessagesConfig) + assert not isinstance(config, TencentAnthropicMessagesConfig) + + +def test_strips_billing_metadata(): + config = TencentAnthropicMessagesConfig() + + assert config.should_strip_billing_metadata() is True + + +def test_get_api_base_default(): + config = TencentAnthropicMessagesConfig() + + assert config.get_api_base() == "https://tokenhub-intl.tencentcloudmaas.com" + + +def test_get_api_base_from_arg(): + config = TencentAnthropicMessagesConfig() + + assert config.get_api_base(api_base="https://custom.example.com") == "https://custom.example.com" + + +def test_messages_url_default(): + config = TencentAnthropicMessagesConfig() + + assert ( + config.get_complete_url( + api_base=None, + api_key=None, + model="deepseek-v4-pro", + optional_params={}, + litellm_params={}, + ) + == "https://tokenhub-intl.tencentcloudmaas.com/v1/messages" + ) + + +def test_messages_url_with_base_ending_in_v1(): + config = TencentAnthropicMessagesConfig() + + assert ( + config.get_complete_url( + api_base="https://tokenhub-intl.tencentcloudmaas.com/v1", + api_key=None, + model="deepseek-v4-pro", + optional_params={}, + litellm_params={}, + ) + == "https://tokenhub-intl.tencentcloudmaas.com/v1/messages" + ) + + +def test_messages_url_with_base_ending_in_v1_messages(): + config = TencentAnthropicMessagesConfig() + + url = config.get_complete_url( + api_base="https://tokenhub-intl.tencentcloudmaas.com/v1/messages", + api_key=None, + model="deepseek-v4-pro", + optional_params={}, + litellm_params={}, + ) + + assert url == "https://tokenhub-intl.tencentcloudmaas.com/v1/messages" + + +def test_messages_url_with_base_ending_in_v1_chat_completions(): + config = TencentAnthropicMessagesConfig() + + assert ( + config.get_complete_url( + api_base="https://tokenhub-intl.tencentcloudmaas.com/v1/chat/completions", + api_key=None, + model="deepseek-v4-pro", + optional_params={}, + litellm_params={}, + ) + == "https://tokenhub-intl.tencentcloudmaas.com/v1/messages" + ) + + +def test_messages_url_with_custom_base_no_v1(): + config = TencentAnthropicMessagesConfig() + + assert ( + config.get_complete_url( + api_base="https://tokenhub.tencentcloudmaas.com", + api_key=None, + model="deepseek-v4-pro", + optional_params={}, + litellm_params={}, + ) + == "https://tokenhub.tencentcloudmaas.com/v1/messages" + ) + + +def test_validate_environment_sets_headers(): + config = TencentAnthropicMessagesConfig() + + headers, api_base = config.validate_anthropic_messages_environment( + headers={}, + model="deepseek-v4-pro", + messages=[], + optional_params={}, + litellm_params={}, + api_key="sk-tencent-key", + api_base="https://custom.test", + ) + + assert headers["x-api-key"] == "sk-tencent-key" + assert headers["anthropic-version"] == "2023-06-01" + assert headers["content-type"] == "application/json" + assert api_base == "https://custom.test" + + +def test_validate_environment_injects_anthropic_beta_headers(): + config = TencentAnthropicMessagesConfig() + + headers, _ = config.validate_anthropic_messages_environment( + headers={}, + model="deepseek-v4-pro", + messages=[], + optional_params={"speed": "fast"}, + litellm_params={}, + api_key="sk-tencent-key", + api_base=None, + ) + + assert "anthropic-beta" in headers + + +def test_validate_environment_preserves_existing_headers(): + config = TencentAnthropicMessagesConfig() + + headers, _ = config.validate_anthropic_messages_environment( + headers={"authorization": "Bearer existing", "anthropic-version": "2024-01-01"}, + model="deepseek-v4-pro", + messages=[], + optional_params={}, + litellm_params={}, + api_key="sk-tencent-key", + api_base=None, + ) + + assert headers["authorization"] == "Bearer existing" + assert headers["anthropic-version"] == "2024-01-01" + assert "x-api-key" not in headers diff --git a/tests/test_litellm/llms/tencent/test_cost_calculator.py b/tests/test_litellm/llms/tencent/test_cost_calculator.py new file mode 100644 index 00000000000..c2e905fab85 --- /dev/null +++ b/tests/test_litellm/llms/tencent/test_cost_calculator.py @@ -0,0 +1,41 @@ +import pytest + +import litellm +from litellm.llms.tencent.cost_calculator import cost_per_token +from litellm.types.utils import Usage + + +@pytest.fixture +def local_model_cost_map(monkeypatch): + original_model_cost = litellm.model_cost + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + litellm.model_cost = litellm.get_model_cost_map(url="") + litellm.get_model_info.cache_clear() + try: + yield + finally: + litellm.model_cost = original_model_cost + litellm.get_model_info.cache_clear() + + +def test_cost_per_token_uses_tencent_model_pricing(local_model_cost_map): + usage = Usage(prompt_tokens=1000, completion_tokens=2000, total_tokens=3000) + + prompt_cost, completion_cost = cost_per_token(model="tencent/deepseek-v4-pro", usage=usage) + + assert prompt_cost == pytest.approx(1000 * 4.35e-07) + assert completion_cost == pytest.approx(2000 * 8.7e-07) + + +def test_top_level_dispatcher_routes_tencent_to_wrapper(local_model_cost_map): + from litellm.cost_calculator import cost_per_token as dispatch_cost_per_token + + prompt_cost, completion_cost = dispatch_cost_per_token( + model="tencent/deepseek-v4-pro", + prompt_tokens=1000, + completion_tokens=1000, + custom_llm_provider="tencent", + ) + + assert prompt_cost == pytest.approx(1000 * 4.35e-07) + assert completion_cost == pytest.approx(1000 * 8.7e-07) diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index 6bc477655d5..c35fb2fcbe2 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -4577,3 +4577,95 @@ def test_aws_bedrock_project_id_excluded_from_bedrock_optional_params(): assert "aws_bedrock_project_id" not in result assert result["aws_region_name"] == "us-east-1" + + +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.""" + from unittest.mock import patch + + 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", + thinking={"type": "enabled"}, + ) + assert result.get("thinking") == {"type": "enabled"} + + def test_tencent_supports_reasoning_effort(self): + """Verify get_optional_params for tencent converts reasoning_effort to thinking.""" + from unittest.mock import patch + + 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", + reasoning_effort="medium", + ) + assert result.get("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 unittest.mock import patch + + from litellm.litellm_core_utils.get_supported_openai_params import ( + get_supported_openai_params, + ) + + with patch( + "litellm.llms.tencent.chat.transformation.supports_reasoning", + return_value=True, + ): + params = get_supported_openai_params( + model="tencent/deepseek-v4-pro", + custom_llm_provider="tencent", + ) + assert "thinking" in params + assert "reasoning_effort" in params + + def test_tencent_messages_config_routing(self): + """Verify ProviderConfigManager routes tencent to TencentAnthropicMessagesConfig.""" + import litellm + from litellm.llms.tencent.messages.transformation import ( + TencentAnthropicMessagesConfig, + ) + from litellm.utils import ProviderConfigManager + + config = ProviderConfigManager.get_provider_anthropic_messages_config( + model="deepseek-v4-pro", + provider=litellm.LlmProviders.TENCENT, + ) + assert isinstance(config, TencentAnthropicMessagesConfig) + assert config.custom_llm_provider == "tencent" + + +class TestValidateEnvironmentTencent: + """Tests that validate_environment resolves TENCENT_API_KEY for the tencent provider.""" + + def test_reports_key_present(self): + with patch.dict(os.environ, {"TENCENT_API_KEY": "sk-tencent"}): + result = litellm.validate_environment(model="tencent/deepseek-v4-pro") + + assert result["keys_in_environment"] is True + assert result["missing_keys"] == [] + + def test_reports_key_missing(self): + with patch.dict(os.environ, {}, clear=True): + result = litellm.validate_environment(model="tencent/deepseek-v4-pro") + + assert result["keys_in_environment"] is False + assert "TENCENT_API_KEY" in result["missing_keys"] + +