mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
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:
parent
b9df7fa705
commit
8bb4e62412
24 changed files with 837 additions and 0 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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
|
||||
(
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
0
litellm/llms/tencent/__init__.py
Normal file
0
litellm/llms/tencent/__init__.py
Normal file
0
litellm/llms/tencent/chat/__init__.py
Normal file
0
litellm/llms/tencent/chat/__init__.py
Normal file
68
litellm/llms/tencent/chat/transformation.py
Normal file
68
litellm/llms/tencent/chat/transformation.py
Normal 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"
|
||||
6
litellm/llms/tencent/cost_calculator.py
Normal file
6
litellm/llms/tencent/cost_calculator.py
Normal 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")
|
||||
0
litellm/llms/tencent/messages/__init__.py
Normal file
0
litellm/llms/tencent/messages/__init__.py
Normal file
85
litellm/llms/tencent/messages/transformation.py
Normal file
85
litellm/llms/tencent/messages/transformation.py
Normal 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"
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
0
tests/test_litellm/llms/tencent/__init__.py
Normal file
0
tests/test_litellm/llms/tencent/__init__.py
Normal file
0
tests/test_litellm/llms/tencent/chat/__init__.py
Normal file
0
tests/test_litellm/llms/tencent/chat/__init__.py
Normal 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"
|
||||
0
tests/test_litellm/llms/tencent/messages/__init__.py
Normal file
0
tests/test_litellm/llms/tencent/messages/__init__.py
Normal 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
|
||||
41
tests/test_litellm/llms/tencent/test_cost_calculator.py
Normal file
41
tests/test_litellm/llms/tencent/test_cost_calculator.py
Normal 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)
|
||||
|
|
@ -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"]
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue