mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-28 06:23:07 +00:00
Merge 4a1f465c52 into b6f91a221b
This commit is contained in:
commit
12ca544f51
17 changed files with 755 additions and 7 deletions
|
|
@ -1051,6 +1051,7 @@ openai_compatible_endpoints: Final[list[str]] = [
|
|||
"https://api.libertai.io/v1",
|
||||
"https://pinstripes.io/v1",
|
||||
"https://api.meta.ai/v1",
|
||||
"https://api-inference.bitdeer.ai/v1",
|
||||
"https://api.sailresearch.com/v1",
|
||||
"https://api.cognition.ai/v1",
|
||||
"https://inference.coralbricks.ai/v1",
|
||||
|
|
@ -1131,6 +1132,7 @@ openai_compatible_providers: Final[list[str]] = [
|
|||
"openinfer",
|
||||
"darkbloom",
|
||||
"meta", # Meta Model API (Muse Spark) - JSON-configured provider
|
||||
"bitdeer-ai", # Bitdeer AI - JSON-configured provider
|
||||
"cognition",
|
||||
"coralbricks",
|
||||
"cortecs",
|
||||
|
|
|
|||
|
|
@ -489,6 +489,12 @@ def image_generation(
|
|||
or custom_llm_provider == LlmProviders.LITELLM_PROXY.value
|
||||
or custom_llm_provider in litellm.openai_compatible_providers
|
||||
):
|
||||
if custom_llm_provider == "bitdeer-ai" and not (api_key or dynamic_api_key):
|
||||
raise litellm.AuthenticationError(
|
||||
message="Bitdeer AI requires api_key or BITDEER_API_KEY",
|
||||
llm_provider=custom_llm_provider,
|
||||
model=model,
|
||||
)
|
||||
if extra_headers is not None:
|
||||
optional_params["extra_headers"] = extra_headers
|
||||
# Forward OpenAI organization if present (set by proxy pre-call utils)
|
||||
|
|
|
|||
|
|
@ -383,6 +383,14 @@ def get_llm_provider(
|
|||
elif endpoint == "https://llm.chutes.ai/v1/":
|
||||
custom_llm_provider = "chutes"
|
||||
dynamic_api_key = get_secret_str("CHUTES_API_KEY")
|
||||
elif endpoint == "https://api-inference.bitdeer.ai/v1":
|
||||
return _get_openai_compatible_provider_info(
|
||||
model=f"bitdeer-ai/{model}",
|
||||
api_base=api_base,
|
||||
api_key=api_key,
|
||||
dynamic_api_key=dynamic_api_key,
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
elif endpoint == "https://api.v0.dev/v1":
|
||||
custom_llm_provider = "v0"
|
||||
dynamic_api_key = get_secret_str("V0_API_KEY")
|
||||
|
|
|
|||
|
|
@ -67,6 +67,13 @@ def create_config_class(provider: SimpleProviderConfig):
|
|||
if not resolved_base:
|
||||
resolved_base = provider.base_url
|
||||
|
||||
if (
|
||||
provider.special_handling.get("require_api_key_for_custom_base")
|
||||
and resolved_base.rstrip("/") != provider.base_url.rstrip("/")
|
||||
and not api_key
|
||||
):
|
||||
raise ValueError(f"An explicit api_key is required for {provider.slug} when overriding api_base")
|
||||
|
||||
# Resolve API key
|
||||
resolved_key: Final = api_key or get_secret_str(provider.api_key_env)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ JSON-based provider configuration loader for OpenAI-compatible providers.
|
|||
"""
|
||||
|
||||
import json
|
||||
from collections.abc import Mapping, Sequence
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
|
|
@ -20,13 +21,13 @@ class SimpleProviderConfig:
|
|||
def __init__(self, slug: str, data: dict):
|
||||
self.slug = slug
|
||||
self.base_url = data["base_url"]
|
||||
self.api_key_env = data["api_key_env"]
|
||||
self.api_base_env = data.get("api_base_env")
|
||||
self.base_class = data.get("base_class", "openai_gpt")
|
||||
self.param_mappings = data.get("param_mappings", {})
|
||||
self.api_key_env: str = data["api_key_env"]
|
||||
self.api_base_env: str | None = data.get("api_base_env")
|
||||
self.base_class: str = data.get("base_class", "openai_gpt")
|
||||
self.param_mappings: Mapping[str, str] = data.get("param_mappings", {})
|
||||
self.constraints = data.get("constraints", {})
|
||||
self.special_handling = data.get("special_handling", {})
|
||||
self.supported_endpoints = data.get("supported_endpoints", [])
|
||||
self.special_handling: Mapping[str, bool] = data.get("special_handling", {})
|
||||
self.supported_endpoints: Sequence[str] = data.get("supported_endpoints", [])
|
||||
self.provider_info: ProviderSpecificModelInfo = (
|
||||
_PROVIDER_INFO_ADAPTER.validate_python(data["provider_info"]) if "provider_info" in data else {}
|
||||
)
|
||||
|
|
@ -86,6 +87,11 @@ class JSONProviderRegistry:
|
|||
return False
|
||||
return "/v1/responses" in provider.supported_endpoints
|
||||
|
||||
@classmethod
|
||||
def supports_embeddings(cls, slug: str) -> bool:
|
||||
provider: Final = cls._providers.get(slug)
|
||||
return provider is not None and "/v1/embeddings" in provider.supported_endpoints
|
||||
|
||||
@classmethod
|
||||
def list_providers(cls) -> list:
|
||||
"""List all registered provider slugs"""
|
||||
|
|
|
|||
|
|
@ -1,4 +1,15 @@
|
|||
{
|
||||
"bitdeer-ai": {
|
||||
"base_url": "https://api-inference.bitdeer.ai/v1",
|
||||
"api_key_env": "BITDEER_API_KEY",
|
||||
"param_mappings": {
|
||||
"max_completion_tokens": "max_tokens"
|
||||
},
|
||||
"special_handling": {
|
||||
"require_api_key_for_custom_base": true
|
||||
},
|
||||
"supported_endpoints": ["/v1/chat/completions", "/v1/embeddings", "/v1/rerank", "/v1/images/generations"]
|
||||
},
|
||||
"publicai": {
|
||||
"base_url": "https://api.publicai.co/v1",
|
||||
"api_key_env": "PUBLICAI_API_KEY",
|
||||
|
|
|
|||
22
litellm/llms/openai_like/rerank/bitdeer_transformation.py
Normal file
22
litellm/llms/openai_like/rerank/bitdeer_transformation.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
from typing import Final
|
||||
|
||||
from litellm.llms.hosted_vllm.rerank.transformation import HostedVLLMRerankConfig
|
||||
from litellm.types.rerank import RerankBilledUnits
|
||||
from litellm.types.utils import ModelInfo
|
||||
|
||||
|
||||
class BitdeerRerankConfig(HostedVLLMRerankConfig):
|
||||
def calculate_rerank_cost(
|
||||
self,
|
||||
model: str,
|
||||
custom_llm_provider: str | None = None,
|
||||
billed_units: RerankBilledUnits | None = None,
|
||||
model_info: ModelInfo | None = None,
|
||||
) -> tuple[float, float]:
|
||||
if model_info is None or billed_units is None:
|
||||
return 0.0, 0.0
|
||||
cost_per_token: Final = model_info.get("input_cost_per_token")
|
||||
total_tokens: Final = billed_units.get("total_tokens")
|
||||
if cost_per_token is None or total_tokens is None:
|
||||
return 0.0, 0.0
|
||||
return cost_per_token * total_tokens, 0.0
|
||||
|
|
@ -2641,6 +2641,12 @@ def _complete_custom_openai(
|
|||
or None # default - https://github.com/openai/openai-python/blob/284c1799070c723c6a553337134148a7ab088dd8/openai/util.py#L105
|
||||
)
|
||||
openai.organization = organization
|
||||
if custom_llm_provider == "bitdeer-ai" and not api_key:
|
||||
raise litellm.AuthenticationError(
|
||||
message="Bitdeer AI requires api_key or BITDEER_API_KEY",
|
||||
llm_provider=custom_llm_provider,
|
||||
model=model,
|
||||
)
|
||||
# set API KEY
|
||||
api_key = (
|
||||
api_key
|
||||
|
|
@ -7646,6 +7652,27 @@ def embedding(
|
|||
aembedding=aembedding,
|
||||
litellm_params={},
|
||||
)
|
||||
elif JSONProviderRegistry.supports_embeddings(custom_llm_provider):
|
||||
if not api_key:
|
||||
raise litellm.AuthenticationError(
|
||||
message=f"{custom_llm_provider} embeddings need an api_key",
|
||||
llm_provider=custom_llm_provider,
|
||||
model=model,
|
||||
)
|
||||
response = openai_chat_completions.embedding( # rebind-ok: one assignment per provider branch
|
||||
model=model,
|
||||
input=input,
|
||||
api_base=api_base,
|
||||
api_key=api_key,
|
||||
logging_obj=logging,
|
||||
timeout=timeout,
|
||||
model_response=EmbeddingResponse(),
|
||||
optional_params=optional_params,
|
||||
client=client,
|
||||
aembedding=aembedding,
|
||||
max_retries=max_retries,
|
||||
litellm_params=litellm_params_dict,
|
||||
)
|
||||
else:
|
||||
raise LiteLLMUnknownProvider(model=model, custom_llm_provider=custom_llm_provider)
|
||||
if response is not None and hasattr(response, "_hidden_params") and isinstance(response, EmbeddingResponse):
|
||||
|
|
|
|||
|
|
@ -63476,6 +63476,114 @@
|
|||
],
|
||||
"supports_audio_output": true
|
||||
},
|
||||
"bitdeer-ai/deepseek-ai/DeepSeek-V4.1-Flash": {
|
||||
"max_input_tokens": 1048576,
|
||||
"input_cost_per_token": 3e-07,
|
||||
"output_cost_per_token": 1.2e-06,
|
||||
"cache_read_input_token_cost": 6e-09,
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"mode": "chat",
|
||||
"supports_function_calling": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_vision": true,
|
||||
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
|
||||
},
|
||||
"bitdeer-ai/zai-org/GLM-5.3": {
|
||||
"max_input_tokens": 1048576,
|
||||
"input_cost_per_token": 1.4e-06,
|
||||
"output_cost_per_token": 4.4e-06,
|
||||
"cache_read_input_token_cost": 2.6e-07,
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"mode": "chat",
|
||||
"supports_function_calling": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_prompt_caching": true,
|
||||
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
|
||||
},
|
||||
"bitdeer-ai/zai-org/GLM-5.3-Flash": {
|
||||
"max_input_tokens": 1048576,
|
||||
"input_cost_per_token": 1.5e-07,
|
||||
"output_cost_per_token": 5e-07,
|
||||
"cache_read_input_token_cost": 3e-08,
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"mode": "chat",
|
||||
"supports_function_calling": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_vision": true,
|
||||
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
|
||||
},
|
||||
"bitdeer-ai/Qwen/Qwen3.8-27B": {
|
||||
"max_input_tokens": 262144,
|
||||
"input_cost_per_token": 5e-07,
|
||||
"output_cost_per_token": 3e-06,
|
||||
"cache_read_input_token_cost": 1e-07,
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"mode": "chat",
|
||||
"supports_function_calling": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_vision": true,
|
||||
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
|
||||
},
|
||||
"bitdeer-ai/deepseek-ai/DeepSeek-V4-Flash": {
|
||||
"max_input_tokens": 1048576,
|
||||
"input_cost_per_token": 1.4e-07,
|
||||
"output_cost_per_token": 2.8e-07,
|
||||
"cache_read_input_token_cost": 1e-08,
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"mode": "chat",
|
||||
"supports_function_calling": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_prompt_caching": true,
|
||||
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
|
||||
},
|
||||
"bitdeer-ai/deepseek-ai/DeepSeek-V4.1-Flash-1017": {
|
||||
"max_input_tokens": 1048576,
|
||||
"input_cost_per_token": 3e-07,
|
||||
"output_cost_per_token": 1.2e-06,
|
||||
"cache_read_input_token_cost": 6e-09,
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"mode": "chat",
|
||||
"supports_function_calling": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_vision": true,
|
||||
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
|
||||
},
|
||||
"bitdeer-ai/BAAI/bge-m3": {
|
||||
"max_input_tokens": 8192,
|
||||
"input_cost_per_token": 1e-08,
|
||||
"output_cost_per_token": 0.0,
|
||||
"output_vector_size": 1024,
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"mode": "embedding",
|
||||
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
|
||||
},
|
||||
"bitdeer-ai/BAAI/bge-reranker-v2-m3": {
|
||||
"max_input_tokens": 8192,
|
||||
"input_cost_per_token": 1e-08,
|
||||
"output_cost_per_token": 0.0,
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"mode": "rerank",
|
||||
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
|
||||
},
|
||||
"bitdeer-ai/seedream-5.0-lite": {
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"mode": "image_generation",
|
||||
"output_cost_per_image": 0.035,
|
||||
"supported_endpoints": [
|
||||
"/v1/images/generations"
|
||||
],
|
||||
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
|
||||
},
|
||||
"fallback_generalizations": {
|
||||
"rules": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -32,6 +32,23 @@
|
|||
}
|
||||
},
|
||||
"providers": {
|
||||
"bitdeer-ai": {
|
||||
"display_name": "Bitdeer AI (`bitdeer-ai`)",
|
||||
"url": "https://docs.litellm.ai/docs/providers/bitdeer",
|
||||
"endpoints": {
|
||||
"chat_completions": true,
|
||||
"messages": true,
|
||||
"responses": true,
|
||||
"embeddings": true,
|
||||
"image_generations": true,
|
||||
"audio_transcriptions": false,
|
||||
"audio_speech": false,
|
||||
"moderations": false,
|
||||
"batches": false,
|
||||
"rerank": true,
|
||||
"a2a": false
|
||||
}
|
||||
},
|
||||
"a2a": {
|
||||
"display_name": "A2A (Agent-to-Agent) (`a2a`)",
|
||||
"url": "https://docs.litellm.ai/docs/providers/a2a",
|
||||
|
|
|
|||
|
|
@ -794,6 +794,34 @@
|
|||
],
|
||||
"default_model_placeholder": "bespoke/nimble"
|
||||
},
|
||||
{
|
||||
"provider": "BITDEER_AI",
|
||||
"provider_display_name": "Bitdeer AI",
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"credential_fields": [
|
||||
{
|
||||
"key": "api_base",
|
||||
"label": "API Base",
|
||||
"placeholder": "https://api-inference.bitdeer.ai/v1",
|
||||
"tooltip": null,
|
||||
"required": false,
|
||||
"field_type": "text",
|
||||
"options": null,
|
||||
"default_value": null
|
||||
},
|
||||
{
|
||||
"key": "api_key",
|
||||
"label": "API Key",
|
||||
"placeholder": null,
|
||||
"tooltip": null,
|
||||
"required": true,
|
||||
"field_type": "password",
|
||||
"options": null,
|
||||
"default_value": null
|
||||
}
|
||||
],
|
||||
"default_model_placeholder": "bitdeer-ai/deepseek-ai/DeepSeek-V4-Flash"
|
||||
},
|
||||
{
|
||||
"provider": "BYTEZ",
|
||||
"provider_display_name": "Bytez",
|
||||
|
|
|
|||
|
|
@ -412,6 +412,28 @@ def rerank(
|
|||
litellm_params=rerank_litellm_params,
|
||||
)
|
||||
|
||||
elif _custom_llm_provider == litellm.LlmProviders.BITDEER_AI:
|
||||
if not dynamic_api_key:
|
||||
raise litellm.AuthenticationError(
|
||||
message="bitdeer-ai rerank needs an api_key or BITDEER_API_KEY",
|
||||
llm_provider="bitdeer-ai",
|
||||
model=model,
|
||||
)
|
||||
response = base_llm_http_handler.rerank( # rebind-ok: one assignment per provider branch
|
||||
model=model,
|
||||
custom_llm_provider=_custom_llm_provider,
|
||||
provider_config=rerank_provider_config,
|
||||
optional_rerank_params=optional_rerank_params,
|
||||
logging_obj=litellm_logging_obj,
|
||||
timeout=optional_params.timeout,
|
||||
api_key=dynamic_api_key,
|
||||
api_base=dynamic_api_base,
|
||||
_is_async=_is_async,
|
||||
headers=headers or litellm.headers or {},
|
||||
client=client,
|
||||
model_response=model_response,
|
||||
litellm_params=rerank_litellm_params,
|
||||
)
|
||||
elif _custom_llm_provider == litellm.LlmProviders.DEEPINFRA:
|
||||
api_key = dynamic_api_key or optional_params.api_key or get_secret_str("DEEPINFRA_API_KEY")
|
||||
|
||||
|
|
|
|||
|
|
@ -4541,6 +4541,7 @@ class LlmProviders(str, Enum):
|
|||
XIAOMI_MIMO = "xiaomi_mimo"
|
||||
TENSORMESH = "tensormesh"
|
||||
LIBERTAI = "libertai"
|
||||
BITDEER_AI = "bitdeer-ai"
|
||||
PINSTRIPES = "pinstripes"
|
||||
COGNITION = "cognition"
|
||||
CORALBRICKS = "coralbricks"
|
||||
|
|
|
|||
|
|
@ -9561,7 +9561,7 @@ class ProviderConfigManager:
|
|||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_provider_rerank_config(
|
||||
def get_provider_rerank_config( # noqa: C901 # flat one-branch-per-provider dispatch
|
||||
model: str,
|
||||
provider: LlmProviders,
|
||||
api_base: str | None,
|
||||
|
|
@ -9584,6 +9584,12 @@ class ProviderConfigManager:
|
|||
)
|
||||
elif litellm.LlmProviders.HOSTED_VLLM == provider:
|
||||
return litellm.HostedVLLMRerankConfig()
|
||||
elif litellm.LlmProviders.BITDEER_AI == provider:
|
||||
from litellm.llms.openai_like.rerank.bitdeer_transformation import (
|
||||
BitdeerRerankConfig,
|
||||
)
|
||||
|
||||
return BitdeerRerankConfig()
|
||||
elif litellm.LlmProviders.HUGGINGFACE == provider:
|
||||
return litellm.HuggingFaceRerankConfig()
|
||||
elif litellm.LlmProviders.DEEPINFRA == provider:
|
||||
|
|
|
|||
|
|
@ -63476,6 +63476,114 @@
|
|||
],
|
||||
"supports_audio_output": true
|
||||
},
|
||||
"bitdeer-ai/deepseek-ai/DeepSeek-V4.1-Flash": {
|
||||
"max_input_tokens": 1048576,
|
||||
"input_cost_per_token": 3e-07,
|
||||
"output_cost_per_token": 1.2e-06,
|
||||
"cache_read_input_token_cost": 6e-09,
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"mode": "chat",
|
||||
"supports_function_calling": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_vision": true,
|
||||
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
|
||||
},
|
||||
"bitdeer-ai/zai-org/GLM-5.3": {
|
||||
"max_input_tokens": 1048576,
|
||||
"input_cost_per_token": 1.4e-06,
|
||||
"output_cost_per_token": 4.4e-06,
|
||||
"cache_read_input_token_cost": 2.6e-07,
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"mode": "chat",
|
||||
"supports_function_calling": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_prompt_caching": true,
|
||||
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
|
||||
},
|
||||
"bitdeer-ai/zai-org/GLM-5.3-Flash": {
|
||||
"max_input_tokens": 1048576,
|
||||
"input_cost_per_token": 1.5e-07,
|
||||
"output_cost_per_token": 5e-07,
|
||||
"cache_read_input_token_cost": 3e-08,
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"mode": "chat",
|
||||
"supports_function_calling": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_vision": true,
|
||||
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
|
||||
},
|
||||
"bitdeer-ai/Qwen/Qwen3.8-27B": {
|
||||
"max_input_tokens": 262144,
|
||||
"input_cost_per_token": 5e-07,
|
||||
"output_cost_per_token": 3e-06,
|
||||
"cache_read_input_token_cost": 1e-07,
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"mode": "chat",
|
||||
"supports_function_calling": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_vision": true,
|
||||
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
|
||||
},
|
||||
"bitdeer-ai/deepseek-ai/DeepSeek-V4-Flash": {
|
||||
"max_input_tokens": 1048576,
|
||||
"input_cost_per_token": 1.4e-07,
|
||||
"output_cost_per_token": 2.8e-07,
|
||||
"cache_read_input_token_cost": 1e-08,
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"mode": "chat",
|
||||
"supports_function_calling": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_prompt_caching": true,
|
||||
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
|
||||
},
|
||||
"bitdeer-ai/deepseek-ai/DeepSeek-V4.1-Flash-1017": {
|
||||
"max_input_tokens": 1048576,
|
||||
"input_cost_per_token": 3e-07,
|
||||
"output_cost_per_token": 1.2e-06,
|
||||
"cache_read_input_token_cost": 6e-09,
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"mode": "chat",
|
||||
"supports_function_calling": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_vision": true,
|
||||
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
|
||||
},
|
||||
"bitdeer-ai/BAAI/bge-m3": {
|
||||
"max_input_tokens": 8192,
|
||||
"input_cost_per_token": 1e-08,
|
||||
"output_cost_per_token": 0.0,
|
||||
"output_vector_size": 1024,
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"mode": "embedding",
|
||||
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
|
||||
},
|
||||
"bitdeer-ai/BAAI/bge-reranker-v2-m3": {
|
||||
"max_input_tokens": 8192,
|
||||
"input_cost_per_token": 1e-08,
|
||||
"output_cost_per_token": 0.0,
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"mode": "rerank",
|
||||
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
|
||||
},
|
||||
"bitdeer-ai/seedream-5.0-lite": {
|
||||
"litellm_provider": "bitdeer-ai",
|
||||
"mode": "image_generation",
|
||||
"output_cost_per_image": 0.035,
|
||||
"supported_endpoints": [
|
||||
"/v1/images/generations"
|
||||
],
|
||||
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
|
||||
},
|
||||
"fallback_generalizations": {
|
||||
"rules": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -33,6 +33,23 @@
|
|||
}
|
||||
},
|
||||
"providers": {
|
||||
"bitdeer-ai": {
|
||||
"display_name": "Bitdeer AI (`bitdeer-ai`)",
|
||||
"url": "https://docs.litellm.ai/docs/providers/bitdeer",
|
||||
"endpoints": {
|
||||
"chat_completions": true,
|
||||
"messages": true,
|
||||
"responses": true,
|
||||
"embeddings": true,
|
||||
"image_generations": true,
|
||||
"audio_transcriptions": false,
|
||||
"audio_speech": false,
|
||||
"moderations": false,
|
||||
"batches": false,
|
||||
"rerank": true,
|
||||
"a2a": false
|
||||
}
|
||||
},
|
||||
"a2a": {
|
||||
"display_name": "A2A (Agent-to-Agent) (`a2a`)",
|
||||
"url": "https://docs.litellm.ai/docs/providers/a2a",
|
||||
|
|
|
|||
352
tests/unit/llms/openai_like/test_bitdeer_provider.py
Normal file
352
tests/unit/llms/openai_like/test_bitdeer_provider.py
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
import json
|
||||
from collections.abc import Mapping, Sequence
|
||||
from pathlib import Path
|
||||
from typing import Final, Literal
|
||||
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
|
||||
from litellm.llms.openai_like.dynamic_config import create_config_class
|
||||
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
|
||||
|
||||
BASE_URL: Final = "https://api-inference.bitdeer.ai/v1"
|
||||
MODEL: Final = "deepseek-ai/DeepSeek-V4-Flash"
|
||||
EMBEDDING_MODEL: Final = "BAAI/bge-m3"
|
||||
RERANK_MODEL: Final = "BAAI/bge-reranker-v2-m3"
|
||||
IMAGE_MODEL: Final = "seedream-5.0-lite"
|
||||
MODELS: Final = (
|
||||
"deepseek-ai/DeepSeek-V4.1-Flash",
|
||||
"deepseek-ai/DeepSeek-V4.1-Flash-1017",
|
||||
"zai-org/GLM-5.3",
|
||||
"zai-org/GLM-5.3-Flash",
|
||||
"Qwen/Qwen3.8-27B",
|
||||
MODEL,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("api_key", (None, "explicit-key"))
|
||||
@pytest.mark.parametrize("api_base", (BASE_URL, BASE_URL + "/"))
|
||||
@pytest.mark.parametrize("routing", ("prefix", "provider", "url"))
|
||||
def test_bitdeer_credentials(monkeypatch: pytest.MonkeyPatch, api_key: str | None, api_base: str, routing: str) -> None:
|
||||
monkeypatch.setenv("BITDEER_API_KEY", "environment-key")
|
||||
|
||||
result: Final = get_llm_provider(
|
||||
model=f"bitdeer-ai/{MODEL}" if routing == "prefix" else MODEL,
|
||||
custom_llm_provider="bitdeer-ai" if routing == "provider" else None,
|
||||
api_base=api_base,
|
||||
api_key=api_key,
|
||||
)
|
||||
|
||||
assert result == (MODEL, "bitdeer-ai", api_key or "environment-key", api_base)
|
||||
|
||||
|
||||
def test_bitdeer_default_base_and_missing_credentials(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delenv("BITDEER_API_KEY", raising=False)
|
||||
assert get_llm_provider(model=f"bitdeer-ai/{MODEL}") == (MODEL, "bitdeer-ai", None, BASE_URL)
|
||||
monkeypatch.setenv("BITDEER_API_KEY", "environment-key")
|
||||
assert get_llm_provider(model=f"bitdeer-ai/{MODEL}") == (MODEL, "bitdeer-ai", "environment-key", BASE_URL)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("api_key", (None, ""))
|
||||
@pytest.mark.parametrize(
|
||||
"api_base",
|
||||
(
|
||||
"https://custom.example/v1",
|
||||
"http://api-inference.bitdeer.ai/v1",
|
||||
"https://api-inference.bitdeer.ai:8443/v1",
|
||||
"https://api-inference.bitdeer.ai.evil.example/v1",
|
||||
"https://api-inference.bitdeer.ai@evil.example/v1",
|
||||
"https://evil.example/https://api-inference.bitdeer.ai/v1",
|
||||
BASE_URL + "/other",
|
||||
BASE_URL + "?redirect=https://evil.example",
|
||||
),
|
||||
)
|
||||
def test_bitdeer_custom_base_requires_explicit_key(
|
||||
monkeypatch: pytest.MonkeyPatch, api_base: str, api_key: str | None
|
||||
) -> None:
|
||||
monkeypatch.setenv("BITDEER_API_KEY", "server-secret")
|
||||
|
||||
with pytest.raises(litellm.BadRequestError, match="explicit api_key is required") as error:
|
||||
get_llm_provider(model=f"bitdeer-ai/{MODEL}", api_base=api_base, api_key=api_key)
|
||||
|
||||
assert "server-secret" not in str(error.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"api_base", ("http://api-inference.bitdeer.ai/v1", "https://api-inference.bitdeer.ai:8443/v1", BASE_URL + "/other")
|
||||
)
|
||||
def test_bitdeer_url_autodetection_rejects_unsafe_key_fallback(monkeypatch: pytest.MonkeyPatch, api_base: str) -> None:
|
||||
monkeypatch.setenv("BITDEER_API_KEY", "server-secret")
|
||||
with pytest.raises(litellm.BadRequestError, match="explicit api_key is required"):
|
||||
get_llm_provider(model=MODEL, api_base=api_base)
|
||||
|
||||
|
||||
def test_bitdeer_dynamic_config_checks_custom_base(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("BITDEER_API_KEY", "server-secret")
|
||||
provider: Final = JSONProviderRegistry.get("bitdeer-ai")
|
||||
assert provider is not None
|
||||
config: Final = create_config_class(provider)()
|
||||
|
||||
with pytest.raises(ValueError, match="explicit api_key is required"):
|
||||
config._get_openai_compatible_provider_info("https://custom.example/v1", None)
|
||||
|
||||
assert config._get_openai_compatible_provider_info("https://custom.example/v1", "explicit-key") == (
|
||||
"https://custom.example/v1",
|
||||
"explicit-key",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", (MODEL, f"bitdeer-ai/{MODEL}"))
|
||||
@pytest.mark.parametrize("api_key", (None, "explicit-key"))
|
||||
def test_bitdeer_completion_sends_selected_key(
|
||||
monkeypatch: pytest.MonkeyPatch, model: str, api_key: str | None
|
||||
) -> None:
|
||||
monkeypatch.setenv("BITDEER_API_KEY", "environment-key")
|
||||
with respx.mock() as upstream:
|
||||
route: Final = upstream.post(BASE_URL + "/chat/completions").respond(
|
||||
200,
|
||||
json={
|
||||
"id": "chatcmpl-bitdeer",
|
||||
"object": "chat.completion",
|
||||
"created": 0,
|
||||
"model": MODEL,
|
||||
"choices": [
|
||||
{"index": 0, "message": {"role": "assistant", "content": "Hello"}, "finish_reason": "stop"}
|
||||
],
|
||||
"usage": {"prompt_tokens": 10, "completion_tokens": 2, "total_tokens": 12},
|
||||
},
|
||||
)
|
||||
response: Final = litellm.completion(
|
||||
model=model,
|
||||
api_base=BASE_URL,
|
||||
api_key=api_key,
|
||||
messages=[{"role": "user", "content": "Hi"}],
|
||||
max_completion_tokens=32,
|
||||
)
|
||||
|
||||
request: Final = route.calls.last.request
|
||||
payload: Final = json.loads(request.content)
|
||||
assert route.call_count == 1
|
||||
assert request.headers["authorization"] == f"Bearer {api_key or 'environment-key'}"
|
||||
assert payload["model"] == MODEL
|
||||
assert payload["max_tokens"] == 32
|
||||
assert "max_completion_tokens" not in payload
|
||||
assert response.choices[0].message.content == "Hello"
|
||||
assert response._hidden_params["response_cost"] == pytest.approx(
|
||||
10 * litellm.model_cost[f"bitdeer-ai/{MODEL}"]["input_cost_per_token"]
|
||||
+ 2 * litellm.model_cost[f"bitdeer-ai/{MODEL}"]["output_cost_per_token"]
|
||||
)
|
||||
|
||||
|
||||
def test_bitdeer_completion_rejects_custom_base_before_sending(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("BITDEER_API_KEY", "server-secret")
|
||||
with respx.mock() as upstream:
|
||||
with pytest.raises(litellm.BadRequestError, match="explicit api_key is required"):
|
||||
litellm.completion(
|
||||
model=f"bitdeer-ai/{MODEL}",
|
||||
api_base="https://custom.example/v1",
|
||||
messages=[{"role": "user", "content": "Hi"}],
|
||||
)
|
||||
assert len(upstream.calls) == 0
|
||||
|
||||
|
||||
def _request(
|
||||
endpoint: Literal["chat", "image"], model: str, custom_llm_provider: str | None = None, api_base: str | None = None
|
||||
) -> object:
|
||||
if endpoint == "chat":
|
||||
return litellm.completion(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
api_base=api_base,
|
||||
messages=[{"role": "user", "content": "Hi"}],
|
||||
)
|
||||
return litellm.image_generation(
|
||||
model=model, custom_llm_provider=custom_llm_provider, api_base=api_base, prompt="A tree"
|
||||
)
|
||||
|
||||
|
||||
async def _arequest(endpoint: Literal["chat", "image"], model: str) -> object:
|
||||
if endpoint == "chat":
|
||||
return await litellm.acompletion(model=model, messages=[{"role": "user", "content": "Hi"}])
|
||||
return await litellm.aimage_generation(model=model, prompt="A tree")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("routing", ("prefix", "provider", "url"))
|
||||
@pytest.mark.parametrize("endpoint", ("chat", "image"))
|
||||
def test_bitdeer_missing_credentials_never_send_openai_key(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
routing: Literal["prefix", "provider", "url"],
|
||||
endpoint: Literal["chat", "image"],
|
||||
) -> None:
|
||||
monkeypatch.delenv("BITDEER_API_KEY", raising=False)
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "openai-secret")
|
||||
monkeypatch.setattr(litellm, "api_key", "global-secret")
|
||||
monkeypatch.setattr(litellm, "openai_key", "global-openai-secret")
|
||||
model: Final = f"bitdeer-ai/{MODEL}" if routing == "prefix" else MODEL
|
||||
provider: Final = "bitdeer-ai" if routing == "provider" else None
|
||||
api_base: Final = BASE_URL if routing == "url" else None
|
||||
|
||||
with respx.mock() as upstream:
|
||||
with pytest.raises(litellm.AuthenticationError, match="BITDEER_API_KEY") as error:
|
||||
_request(endpoint, model=model, custom_llm_provider=provider, api_base=api_base)
|
||||
assert len(upstream.calls) == 0
|
||||
assert "secret" not in str(error.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("endpoint", ("chat", "image"))
|
||||
async def test_bitdeer_async_missing_credentials_never_send_openai_key(
|
||||
monkeypatch: pytest.MonkeyPatch, endpoint: Literal["chat", "image"]
|
||||
) -> None:
|
||||
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||
monkeypatch.delenv("BITDEER_API_KEY", raising=False)
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "openai-secret")
|
||||
monkeypatch.setattr(litellm, "api_key", "global-secret")
|
||||
monkeypatch.setattr(litellm, "openai_key", "global-openai-secret")
|
||||
|
||||
with respx.mock() as upstream:
|
||||
with pytest.raises(litellm.AuthenticationError, match="BITDEER_API_KEY") as error:
|
||||
await _arequest(endpoint, model=f"bitdeer-ai/{MODEL}")
|
||||
assert len(upstream.calls) == 0
|
||||
assert "secret" not in str(error.value)
|
||||
|
||||
|
||||
def _unique_entries(pairs: Sequence[tuple[str, object]]) -> Mapping[str, object]:
|
||||
keys: Final = tuple(key for key, _ in pairs)
|
||||
assert len(keys) == len(set(keys)), "Duplicate JSON key"
|
||||
return dict(pairs)
|
||||
|
||||
|
||||
def test_bitdeer_pricing_and_packaged_registry() -> None:
|
||||
root: Final = Path(litellm.__file__).parent.parent
|
||||
prices: Final = json.loads(
|
||||
(root / "model_prices_and_context_window.json").read_text(), object_pairs_hook=_unique_entries
|
||||
)
|
||||
backup: Final = json.loads((root / "litellm/model_prices_and_context_window_backup.json").read_text())
|
||||
|
||||
for model in (*MODELS, EMBEDDING_MODEL, RERANK_MODEL, IMAGE_MODEL):
|
||||
extra: Final = f"bitdeer-ai/{model}"
|
||||
assert prices[extra] == backup[extra] == litellm.model_cost[extra]
|
||||
assert prices[extra]["litellm_provider"] == "bitdeer-ai"
|
||||
|
||||
for model in MODELS:
|
||||
name: Final = f"bitdeer-ai/{model}"
|
||||
entry: Final = prices[name]
|
||||
assert entry["mode"] == "chat"
|
||||
assert entry["max_input_tokens"] > 0
|
||||
assert entry["litellm_provider"] == "bitdeer-ai"
|
||||
prompt_cost, output_cost = litellm.cost_per_token(model=name, prompt_tokens=100, completion_tokens=10)
|
||||
assert prompt_cost == pytest.approx(100 * entry["input_cost_per_token"])
|
||||
assert output_cost == pytest.approx(10 * entry["output_cost_per_token"])
|
||||
|
||||
|
||||
def test_bitdeer_embedding_sends_selected_key_and_charges_input_tokens(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("BITDEER_API_KEY", "environment-key")
|
||||
with respx.mock() as upstream:
|
||||
route: Final = upstream.post(BASE_URL + "/embeddings").respond(
|
||||
200,
|
||||
json={
|
||||
"object": "list",
|
||||
"model": EMBEDDING_MODEL,
|
||||
"data": [{"object": "embedding", "index": 0, "embedding": [0.25, -0.5]}],
|
||||
"usage": {"prompt_tokens": 7, "total_tokens": 7},
|
||||
},
|
||||
)
|
||||
response: Final = litellm.embedding(model=f"bitdeer-ai/{EMBEDDING_MODEL}", input=["hello"], api_base=BASE_URL)
|
||||
|
||||
request: Final = route.calls.last.request
|
||||
assert json.loads(request.content) == {"model": EMBEDDING_MODEL, "input": ["hello"]}
|
||||
assert request.headers["authorization"] == "Bearer environment-key"
|
||||
assert response.data[0]["embedding"] == [0.25, -0.5]
|
||||
assert response._hidden_params["response_cost"] == pytest.approx(
|
||||
7 * litellm.model_cost[f"bitdeer-ai/{EMBEDDING_MODEL}"]["input_cost_per_token"]
|
||||
)
|
||||
|
||||
|
||||
def test_bitdeer_embedding_without_bitdeer_key_never_sends_the_openai_key(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delenv("BITDEER_API_KEY", raising=False)
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "openai-secret")
|
||||
with respx.mock() as upstream:
|
||||
with pytest.raises(litellm.AuthenticationError, match="api_key"):
|
||||
litellm.embedding(model=f"bitdeer-ai/{EMBEDDING_MODEL}", input=["hello"], api_base=BASE_URL)
|
||||
assert len(upstream.calls) == 0
|
||||
|
||||
|
||||
def test_bitdeer_rerank_orders_results_and_sends_selected_key(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("BITDEER_API_KEY", "environment-key")
|
||||
with respx.mock() as upstream:
|
||||
route: Final = upstream.post(BASE_URL + "/rerank").respond(
|
||||
200,
|
||||
json={
|
||||
"id": "rerank-bitdeer",
|
||||
"model": RERANK_MODEL,
|
||||
"usage": {"total_tokens": 9},
|
||||
"results": [
|
||||
{"index": 1, "relevance_score": 0.91, "document": {"text": "paris"}},
|
||||
{"index": 0, "relevance_score": 0.02, "document": {"text": "berlin"}},
|
||||
],
|
||||
},
|
||||
)
|
||||
response: Final = litellm.rerank(
|
||||
model=f"bitdeer-ai/{RERANK_MODEL}",
|
||||
query="capital of france",
|
||||
documents=["berlin", "paris"],
|
||||
api_key="explicit-key",
|
||||
)
|
||||
|
||||
request: Final = route.calls.last.request
|
||||
payload: Final = json.loads(request.content)
|
||||
assert payload["model"] == RERANK_MODEL
|
||||
assert payload["query"] == "capital of france"
|
||||
assert payload["documents"] == ["berlin", "paris"]
|
||||
assert request.headers["authorization"] == "Bearer explicit-key"
|
||||
assert [(result["index"], result["relevance_score"]) for result in response.results] == [(1, 0.91), (0, 0.02)]
|
||||
assert response._hidden_params["response_cost"] == pytest.approx(
|
||||
9 * litellm.model_cost[f"bitdeer-ai/{RERANK_MODEL}"]["input_cost_per_token"]
|
||||
)
|
||||
|
||||
|
||||
def test_bitdeer_rerank_without_bitdeer_key_never_sends_the_hosted_vllm_key(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delenv("BITDEER_API_KEY", raising=False)
|
||||
monkeypatch.setenv("HOSTED_VLLM_API_KEY", "vllm-secret")
|
||||
with respx.mock() as upstream:
|
||||
with pytest.raises(litellm.AuthenticationError, match="BITDEER_API_KEY"):
|
||||
litellm.rerank(model=f"bitdeer-ai/{RERANK_MODEL}", query="q", documents=["d"])
|
||||
assert len(upstream.calls) == 0
|
||||
|
||||
|
||||
def test_bitdeer_rerank_rejects_custom_base_before_sending(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("BITDEER_API_KEY", "server-secret")
|
||||
with respx.mock() as upstream:
|
||||
with pytest.raises(litellm.BadRequestError, match="explicit api_key is required"):
|
||||
litellm.rerank(
|
||||
model=f"bitdeer-ai/{RERANK_MODEL}",
|
||||
query="q",
|
||||
documents=["d"],
|
||||
api_base="https://custom.example/v1",
|
||||
)
|
||||
assert len(upstream.calls) == 0
|
||||
|
||||
|
||||
def test_bitdeer_image_generation_sends_selected_key_and_charges_per_image(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("BITDEER_API_KEY", "environment-key")
|
||||
with respx.mock() as upstream:
|
||||
route: Final = upstream.post(BASE_URL + "/images/generations").respond(
|
||||
200,
|
||||
json={"created": 0, "data": [{"url": "https://images.example/out.jpeg", "size": "2048x2048"}]},
|
||||
)
|
||||
response: Final = litellm.image_generation(
|
||||
model=f"bitdeer-ai/{IMAGE_MODEL}", prompt="a red circle", api_base=BASE_URL, api_key="explicit-key"
|
||||
)
|
||||
|
||||
request: Final = route.calls.last.request
|
||||
payload: Final = json.loads(request.content)
|
||||
assert payload["model"] == IMAGE_MODEL
|
||||
assert payload["prompt"] == "a red circle"
|
||||
assert request.headers["authorization"] == "Bearer explicit-key"
|
||||
assert response.data[0].url == "https://images.example/out.jpeg"
|
||||
assert response._hidden_params["response_cost"] == pytest.approx(
|
||||
litellm.model_cost[f"bitdeer-ai/{IMAGE_MODEL}"]["output_cost_per_image"]
|
||||
)
|
||||
Loading…
Add table
Reference in a new issue