feat(tencent): add Tencent TokenHub as a provider (#31903)

* feat(tencent): add Tencent TokenHub as a provider

Tencent TokenHub is OpenAI- and Anthropic-compatible. This registers it as a
new provider: TencentChatConfig routes /v1/chat/completions and gates the
thinking/reasoning_effort params behind supports_reasoning, and
TencentAnthropicMessagesConfig routes the Anthropic-compatible Messages API.
Adds cost tracking, the deepseek-v4-pro/flash model entries, and provider
endpoint support metadata.

* test(tencent): add unit tests for Tencent TokenHub provider

Covers TencentChatConfig (chat completions) and TencentAnthropicMessagesConfig
(messages API) across transformation, param mapping, URL building, and header
validation, plus get_optional_params routing. Tests mock supports_reasoning to
stay independent of remote model cost data.

* fix(tencent): correct max_output_tokens and reuse parent messages env validation

Raise max_output_tokens/max_tokens for tencent/deepseek-v4-pro and tencent/deepseek-v4-flash from 8192 to 384000, matching Tencent TokenHub's published DeepSeek-V4 output limit; the 8192 value mirrored the native DeepSeek default and would have rejected valid larger requests before they reached Tencent

Delegate validate_anthropic_messages_environment to the parent via super() so the Tencent messages endpoint keeps content-type and anthropic-beta header injection instead of dropping them, keeping only the TENCENT_API_KEY resolution overridden

Add regression tests covering beta-header injection, the cost-calculator delegation, provider-info secret resolution, and validate_environment key handling

* fix(tencent): normalize messages URL when TENCENT_API_BASE has chat completions suffix

* fix(tencent): register tencent in models_by_provider

The provider was added to the LlmProviders enum and cost map but not to the
models_by_provider lookup, so test_models_by_provider (which asserts every
litellm_provider present in the cost map is registered) failed once the tencent
models were loaded. Add the tencent_models set, populate it from the cost map,
and expose it under the tencent key, mirroring deepseek.

* fix(tencent): import generic_cost_per_token from its canonical module

Import generic_cost_per_token from litellm.litellm_core_utils.llm_cost_calc.utils
instead of the top-level litellm.cost_calculator dispatcher, which imports the
tencent cost module at load time. Removing the back-reference avoids the circular
import and matches how deepseek and the other providers source the helper.

---------

Co-authored-by: Felipe Rodrigues Gare Carnielli <felipe.gare@hotmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
Mateo Wang 2026-07-02 18:31:59 -07:00 • committed by GitHub
parent b9df7fa705
commit 8bb4e62412
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 837 additions and 0 deletions

View file

@ -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]

View file

@ -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",

View file

@ -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",

View file

@ -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":

View file

@ -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
(

View file

@ -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":

View file

View file

View file

@ -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"

View file

@ -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")

View file

@ -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"

View file

@ -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,

View file

@ -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"

View file

@ -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 (

View file

@ -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,

View file

@ -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",

View file

@ -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"

View file

@ -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

View file

@ -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)

View file

@ -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"]