From 192052d2cce410efa02a3cc715d94eb7db15d032 Mon Sep 17 00:00:00 2001 From: ruijie Date: Thu, 13 Aug 2026 00:16:12 +0800 Subject: [PATCH 1/7] feat(providers): add Bitdeer AI as a JSON-configured provider Registers Bitdeer's OpenAI-compatible inference API (api-inference.bitdeer.ai) following the existing JSON-configured provider pattern: providers.json entry, LlmProviders enum member, openai_compatible_providers list entry, and the api_base auto-detection branch in get_llm_provider_logic.py. Adds pricing for 6 models to model_prices_and_context_window.json (Kimi-K3, Kimi-K2.6, GLM-5.2, Qwen3.5-397B-A17B, DeepSeek-V4-Pro, DeepSeek-V4-Flash), sourced from bitdeer.ai's published pricing page. --- litellm/constants.py | 2 + .../get_llm_provider_logic.py | 3 + litellm/llms/openai_like/providers.json | 7 + litellm/types/utils.py | 1 + model_prices_and_context_window.json | 93 +++++++++++++ .../llms/openai_like/test_bitdeer_provider.py | 123 ++++++++++++++++++ 6 files changed, 229 insertions(+) create mode 100644 tests/test_litellm/llms/openai_like/test_bitdeer_provider.py diff --git a/litellm/constants.py b/litellm/constants.py index 8c3541067a5..4bb99cfa532 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -754,6 +754,7 @@ openai_compatible_endpoints: Final[list] = [ "https://api.libertai.io/v1", "https://pinstripes.io/v1", "https://api.meta.ai/v1", + "https://api-inference.bitdeer.ai/v1", ] @@ -821,6 +822,7 @@ openai_compatible_providers: Final[list] = [ "pinstripes", # Pinstripes - JSON-configured provider "darkbloom", "meta", # Meta Model API (Muse Spark) - JSON-configured provider + "bitdeer", # Bitdeer AI - JSON-configured provider ] openai_text_completion_compatible_providers: Final[list] = [ # providers that support `/v1/completions` "together_ai", diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index dbb40913e14..e25c62d0f9f 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -325,6 +325,9 @@ 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": + custom_llm_provider = "bitdeer" + dynamic_api_key = get_secret_str("BITDEER_API_KEY") elif endpoint == "https://api.v0.dev/v1": custom_llm_provider = "v0" dynamic_api_key = get_secret_str("V0_API_KEY") diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index 164100d4194..6fd7926ad63 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -183,5 +183,12 @@ "max_completion_tokens": "max_tokens" }, "supported_endpoints": ["/v1/chat/completions", "/v1/responses", "/v1/embeddings"] + }, + "bitdeer": { + "base_url": "https://api-inference.bitdeer.ai/v1", + "api_key_env": "BITDEER_API_KEY", + "param_mappings": { + "max_completion_tokens": "max_tokens" + } } } diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 354857f8d72..8418a3431d0 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3710,6 +3710,7 @@ class LlmProviders(str, Enum): XIAOMI_MIMO = "xiaomi_mimo" TENSORMESH = "tensormesh" LIBERTAI = "libertai" + BITDEER = "bitdeer" PINSTRIPES = "pinstripes" DARKBLOOM = "darkbloom" META = "meta" diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index b12e4a9fea3..aebf37fbeb6 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -47176,6 +47176,99 @@ ], "supports_audio_output": true }, + "bitdeer/moonshotai/Kimi-K3": { + "max_tokens": 1048576, + "max_input_tokens": 1048576, + "max_output_tokens": 131072, + "input_cost_per_token": 3e-06, + "output_cost_per_token": 1.5e-05, + "cache_read_input_token_cost": 3e-07, + "litellm_provider": "bitdeer", + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "supports_vision": true, + "supports_prompt_caching": true, + "source": "https://www.bitdeer.ai/en/pricing/ai-models" + }, + "bitdeer/zai-org/GLM-5.2": { + "max_tokens": 1000000, + "max_input_tokens": 1000000, + "max_output_tokens": 131072, + "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", + "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/moonshotai/Kimi-K2.6": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "input_cost_per_token": 9.5e-07, + "output_cost_per_token": 4e-06, + "cache_read_input_token_cost": 2e-07, + "litellm_provider": "bitdeer", + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "supports_vision": true, + "supports_prompt_caching": true, + "source": "https://www.bitdeer.ai/en/pricing/ai-models" + }, + "bitdeer/Qwen/Qwen3.5-397B-A17B": { + "max_tokens": 262144, + "max_input_tokens": 262144, + "max_output_tokens": 65536, + "input_cost_per_token": 6e-07, + "output_cost_per_token": 3.55e-06, + "cache_read_input_token_cost": 2e-07, + "litellm_provider": "bitdeer", + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "supports_vision": true, + "supports_prompt_caching": true, + "source": "https://www.bitdeer.ai/en/pricing/ai-models" + }, + "bitdeer/deepseek-ai/DeepSeek-V4-Pro": { + "max_tokens": 1000000, + "max_input_tokens": 1000000, + "max_output_tokens": 384000, + "input_cost_per_token": 1.168e-06, + "output_cost_per_token": 2.336e-06, + "cache_read_input_token_cost": 1e-07, + "litellm_provider": "bitdeer", + "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/deepseek-ai/DeepSeek-V4-Flash": { + "max_tokens": 1000000, + "max_input_tokens": 1000000, + "max_output_tokens": 384000, + "input_cost_per_token": 4e-08, + "output_cost_per_token": 8e-08, + "cache_read_input_token_cost": 1e-08, + "litellm_provider": "bitdeer", + "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" + }, "fallback_generalizations": { "rules": [ { diff --git a/tests/test_litellm/llms/openai_like/test_bitdeer_provider.py b/tests/test_litellm/llms/openai_like/test_bitdeer_provider.py new file mode 100644 index 00000000000..b861c6048bb --- /dev/null +++ b/tests/test_litellm/llms/openai_like/test_bitdeer_provider.py @@ -0,0 +1,123 @@ +""" +Tests for Bitdeer AI provider configuration and integration. +""" + +import json +import os + +import litellm + +workspace_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../..")) + + +class TestBitdeerProviderConfig: + """Test Bitdeer AI provider configuration""" + + def test_bitdeer_in_provider_list(self): + """Test that bitdeer is in the provider list""" + from litellm import LlmProviders + + assert hasattr(LlmProviders, "BITDEER") + assert LlmProviders.BITDEER.value == "bitdeer" + assert "bitdeer" in litellm.provider_list + + def test_bitdeer_json_config_exists(self): + """Test that bitdeer is configured in providers.json""" + from litellm.llms.openai_like.json_loader import JSONProviderRegistry + + assert JSONProviderRegistry.exists("bitdeer") + + bitdeer = JSONProviderRegistry.get("bitdeer") + assert bitdeer is not None + assert bitdeer.base_url == "https://api-inference.bitdeer.ai/v1" + assert bitdeer.api_key_env == "BITDEER_API_KEY" + assert bitdeer.param_mappings.get("max_completion_tokens") == "max_tokens" + + def test_bitdeer_in_openai_compatible_providers(self): + """Test that bitdeer is in the openai_compatible_providers list""" + from litellm.constants import openai_compatible_providers + + assert "bitdeer" in openai_compatible_providers + + def test_bitdeer_provider_resolution(self): + """Test that provider resolution finds bitdeer and returns the default base URL""" + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + model, provider, api_key, api_base = get_llm_provider( + model="bitdeer/moonshotai/Kimi-K3", + custom_llm_provider=None, + api_base=None, + api_key=None, + ) + + assert model == "moonshotai/Kimi-K3" + assert provider == "bitdeer" + assert api_base == "https://api-inference.bitdeer.ai/v1" + + def test_bitdeer_api_base_override(self): + """Test that an explicit api_base / api_key overrides the default""" + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + model, provider, api_key, api_base = get_llm_provider( + model="bitdeer/moonshotai/Kimi-K3", + custom_llm_provider=None, + api_base="https://custom.bitdeer.ai/v1", + api_key="sk-test", + ) + + assert provider == "bitdeer" + assert api_base == "https://custom.bitdeer.ai/v1" + assert api_key == "sk-test" + + def test_bitdeer_url_autodetection(self): + """Test that api_base=api-inference.bitdeer.ai/v1 auto-sets custom_llm_provider=bitdeer""" + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + model, provider, api_key, api_base = get_llm_provider( + model="moonshotai/Kimi-K3", + custom_llm_provider=None, + api_base="https://api-inference.bitdeer.ai/v1", + api_key=None, + ) + assert provider == "bitdeer" + assert api_base == "https://api-inference.bitdeer.ai/v1" + + def test_bitdeer_router_config(self): + """Test that bitdeer can be used in Router configuration""" + from litellm import Router + + router = Router( + model_list=[ + { + "model_name": "bitdeer-kimi-k3", + "litellm_params": { + "model": "bitdeer/moonshotai/Kimi-K3", + "api_key": "test-key", + }, + } + ] + ) + + assert len(router.model_list) == 1 + assert router.model_list[0]["model_name"] == "bitdeer-kimi-k3" + + def test_bitdeer_model_cost_map(self): + """Test that bitdeer model pricing is registered correctly""" + with open( + os.path.join(workspace_path, "model_prices_and_context_window.json") + ) as f: + model_cost = json.load(f) + + expected_models = { + "bitdeer/moonshotai/Kimi-K3": (3e-06, 1.5e-05), + "bitdeer/zai-org/GLM-5.2": (1.4e-06, 4.4e-06), + "bitdeer/moonshotai/Kimi-K2.6": (9.5e-07, 4e-06), + "bitdeer/Qwen/Qwen3.5-397B-A17B": (6e-07, 3.55e-06), + "bitdeer/deepseek-ai/DeepSeek-V4-Pro": (1.168e-06, 2.336e-06), + "bitdeer/deepseek-ai/DeepSeek-V4-Flash": (4e-08, 8e-08), + } + for model, (input_cost, output_cost) in expected_models.items(): + assert model in model_cost + assert model_cost[model]["litellm_provider"] == "bitdeer" + assert model_cost[model]["input_cost_per_token"] == input_cost + assert model_cost[model]["output_cost_per_token"] == output_cost From e4ab197e7088aa54b1166b3a31feea712b60f0e9 Mon Sep 17 00:00:00 2001 From: ruijie Date: Thu, 13 Aug 2026 12:19:31 +0800 Subject: [PATCH 2/7] Trim Bitdeer models to 3 and rename slug to bitdeer-ai Keeps moonshotai/Kimi-K3, zai-org/GLM-5.2, and deepseek-ai/DeepSeek-V4-Flash; drops Kimi-K2.6, Qwen3.5-397B-A17B, and DeepSeek-V4-Pro. Renames the provider slug from bitdeer to bitdeer-ai to match Bitdeer's preferred identifier. --- litellm/constants.py | 2 +- .../get_llm_provider_logic.py | 2 +- litellm/llms/openai_like/providers.json | 2 +- litellm/types/utils.py | 2 +- model_prices_and_context_window.json | 57 +++---------------- .../llms/openai_like/test_bitdeer_provider.py | 47 +++++++-------- 6 files changed, 33 insertions(+), 79 deletions(-) diff --git a/litellm/constants.py b/litellm/constants.py index 4bb99cfa532..5d0001ca89e 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -822,7 +822,7 @@ openai_compatible_providers: Final[list] = [ "pinstripes", # Pinstripes - JSON-configured provider "darkbloom", "meta", # Meta Model API (Muse Spark) - JSON-configured provider - "bitdeer", # Bitdeer AI - JSON-configured provider + "bitdeer-ai", # Bitdeer AI - JSON-configured provider ] openai_text_completion_compatible_providers: Final[list] = [ # providers that support `/v1/completions` "together_ai", diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index e25c62d0f9f..c0986148b75 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -326,7 +326,7 @@ def get_llm_provider( custom_llm_provider = "chutes" dynamic_api_key = get_secret_str("CHUTES_API_KEY") elif endpoint == "https://api-inference.bitdeer.ai/v1": - custom_llm_provider = "bitdeer" + custom_llm_provider = "bitdeer-ai" dynamic_api_key = get_secret_str("BITDEER_API_KEY") elif endpoint == "https://api.v0.dev/v1": custom_llm_provider = "v0" diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index 6fd7926ad63..62ebd86afec 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -184,7 +184,7 @@ }, "supported_endpoints": ["/v1/chat/completions", "/v1/responses", "/v1/embeddings"] }, - "bitdeer": { + "bitdeer-ai": { "base_url": "https://api-inference.bitdeer.ai/v1", "api_key_env": "BITDEER_API_KEY", "param_mappings": { diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 8418a3431d0..45636969a31 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3710,7 +3710,7 @@ class LlmProviders(str, Enum): XIAOMI_MIMO = "xiaomi_mimo" TENSORMESH = "tensormesh" LIBERTAI = "libertai" - BITDEER = "bitdeer" + BITDEER_AI = "bitdeer-ai" PINSTRIPES = "pinstripes" DARKBLOOM = "darkbloom" META = "meta" diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index aebf37fbeb6..e6dfb472aad 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -47176,14 +47176,14 @@ ], "supports_audio_output": true }, - "bitdeer/moonshotai/Kimi-K3": { + "bitdeer-ai/moonshotai/Kimi-K3": { "max_tokens": 1048576, "max_input_tokens": 1048576, "max_output_tokens": 131072, "input_cost_per_token": 3e-06, "output_cost_per_token": 1.5e-05, "cache_read_input_token_cost": 3e-07, - "litellm_provider": "bitdeer", + "litellm_provider": "bitdeer-ai", "mode": "chat", "supports_function_calling": true, "supports_tool_choice": true, @@ -47192,14 +47192,14 @@ "supports_prompt_caching": true, "source": "https://www.bitdeer.ai/en/pricing/ai-models" }, - "bitdeer/zai-org/GLM-5.2": { + "bitdeer-ai/zai-org/GLM-5.2": { "max_tokens": 1000000, "max_input_tokens": 1000000, "max_output_tokens": 131072, "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", + "litellm_provider": "bitdeer-ai", "mode": "chat", "supports_function_calling": true, "supports_tool_choice": true, @@ -47207,61 +47207,18 @@ "supports_prompt_caching": true, "source": "https://www.bitdeer.ai/en/pricing/ai-models" }, - "bitdeer/moonshotai/Kimi-K2.6": { - "max_tokens": 262144, - "max_input_tokens": 262144, - "max_output_tokens": 262144, - "input_cost_per_token": 9.5e-07, - "output_cost_per_token": 4e-06, - "cache_read_input_token_cost": 2e-07, - "litellm_provider": "bitdeer", - "mode": "chat", - "supports_function_calling": true, - "supports_tool_choice": true, - "supports_reasoning": true, - "supports_vision": true, + "bitdeer-ai/deepseek-ai/DeepSeek-V4-Flash": { "supports_prompt_caching": true, "source": "https://www.bitdeer.ai/en/pricing/ai-models" }, - "bitdeer/Qwen/Qwen3.5-397B-A17B": { - "max_tokens": 262144, - "max_input_tokens": 262144, - "max_output_tokens": 65536, - "input_cost_per_token": 6e-07, - "output_cost_per_token": 3.55e-06, - "cache_read_input_token_cost": 2e-07, - "litellm_provider": "bitdeer", - "mode": "chat", - "supports_function_calling": true, - "supports_tool_choice": true, - "supports_reasoning": true, - "supports_vision": true, - "supports_prompt_caching": true, - "source": "https://www.bitdeer.ai/en/pricing/ai-models" - }, - "bitdeer/deepseek-ai/DeepSeek-V4-Pro": { - "max_tokens": 1000000, - "max_input_tokens": 1000000, - "max_output_tokens": 384000, - "input_cost_per_token": 1.168e-06, - "output_cost_per_token": 2.336e-06, - "cache_read_input_token_cost": 1e-07, - "litellm_provider": "bitdeer", - "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/deepseek-ai/DeepSeek-V4-Flash": { + "bitdeer-ai/deepseek-ai/DeepSeek-V4-Flash": { "max_tokens": 1000000, "max_input_tokens": 1000000, "max_output_tokens": 384000, "input_cost_per_token": 4e-08, "output_cost_per_token": 8e-08, "cache_read_input_token_cost": 1e-08, - "litellm_provider": "bitdeer", + "litellm_provider": "bitdeer-ai", "mode": "chat", "supports_function_calling": true, "supports_tool_choice": true, diff --git a/tests/test_litellm/llms/openai_like/test_bitdeer_provider.py b/tests/test_litellm/llms/openai_like/test_bitdeer_provider.py index b861c6048bb..949c47336d0 100644 --- a/tests/test_litellm/llms/openai_like/test_bitdeer_provider.py +++ b/tests/test_litellm/llms/openai_like/test_bitdeer_provider.py @@ -17,41 +17,41 @@ class TestBitdeerProviderConfig: """Test that bitdeer is in the provider list""" from litellm import LlmProviders - assert hasattr(LlmProviders, "BITDEER") - assert LlmProviders.BITDEER.value == "bitdeer" - assert "bitdeer" in litellm.provider_list + assert hasattr(LlmProviders, "BITDEER_AI") + assert LlmProviders.BITDEER_AI.value == "bitdeer-ai" + assert "bitdeer-ai" in litellm.provider_list def test_bitdeer_json_config_exists(self): """Test that bitdeer is configured in providers.json""" from litellm.llms.openai_like.json_loader import JSONProviderRegistry - assert JSONProviderRegistry.exists("bitdeer") + assert JSONProviderRegistry.exists("bitdeer-ai") - bitdeer = JSONProviderRegistry.get("bitdeer") - assert bitdeer is not None - assert bitdeer.base_url == "https://api-inference.bitdeer.ai/v1" - assert bitdeer.api_key_env == "BITDEER_API_KEY" - assert bitdeer.param_mappings.get("max_completion_tokens") == "max_tokens" + bitdeer_ai = JSONProviderRegistry.get("bitdeer-ai") + assert bitdeer_ai is not None + assert bitdeer_ai.base_url == "https://api-inference.bitdeer.ai/v1" + assert bitdeer_ai.api_key_env == "BITDEER_API_KEY" + assert bitdeer_ai.param_mappings.get("max_completion_tokens") == "max_tokens" def test_bitdeer_in_openai_compatible_providers(self): """Test that bitdeer is in the openai_compatible_providers list""" from litellm.constants import openai_compatible_providers - assert "bitdeer" in openai_compatible_providers + assert "bitdeer-ai" in openai_compatible_providers def test_bitdeer_provider_resolution(self): """Test that provider resolution finds bitdeer and returns the default base URL""" from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider model, provider, api_key, api_base = get_llm_provider( - model="bitdeer/moonshotai/Kimi-K3", + model="bitdeer-ai/moonshotai/Kimi-K3", custom_llm_provider=None, api_base=None, api_key=None, ) assert model == "moonshotai/Kimi-K3" - assert provider == "bitdeer" + assert provider == "bitdeer-ai" assert api_base == "https://api-inference.bitdeer.ai/v1" def test_bitdeer_api_base_override(self): @@ -59,13 +59,13 @@ class TestBitdeerProviderConfig: from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider model, provider, api_key, api_base = get_llm_provider( - model="bitdeer/moonshotai/Kimi-K3", + model="bitdeer-ai/moonshotai/Kimi-K3", custom_llm_provider=None, api_base="https://custom.bitdeer.ai/v1", api_key="sk-test", ) - assert provider == "bitdeer" + assert provider == "bitdeer-ai" assert api_base == "https://custom.bitdeer.ai/v1" assert api_key == "sk-test" @@ -79,7 +79,7 @@ class TestBitdeerProviderConfig: api_base="https://api-inference.bitdeer.ai/v1", api_key=None, ) - assert provider == "bitdeer" + assert provider == "bitdeer-ai" assert api_base == "https://api-inference.bitdeer.ai/v1" def test_bitdeer_router_config(self): @@ -89,9 +89,9 @@ class TestBitdeerProviderConfig: router = Router( model_list=[ { - "model_name": "bitdeer-kimi-k3", + "model_name": "bitdeer-ai-kimi-k3", "litellm_params": { - "model": "bitdeer/moonshotai/Kimi-K3", + "model": "bitdeer-ai/moonshotai/Kimi-K3", "api_key": "test-key", }, } @@ -99,7 +99,7 @@ class TestBitdeerProviderConfig: ) assert len(router.model_list) == 1 - assert router.model_list[0]["model_name"] == "bitdeer-kimi-k3" + assert router.model_list[0]["model_name"] == "bitdeer-ai-kimi-k3" def test_bitdeer_model_cost_map(self): """Test that bitdeer model pricing is registered correctly""" @@ -109,15 +109,12 @@ class TestBitdeerProviderConfig: model_cost = json.load(f) expected_models = { - "bitdeer/moonshotai/Kimi-K3": (3e-06, 1.5e-05), - "bitdeer/zai-org/GLM-5.2": (1.4e-06, 4.4e-06), - "bitdeer/moonshotai/Kimi-K2.6": (9.5e-07, 4e-06), - "bitdeer/Qwen/Qwen3.5-397B-A17B": (6e-07, 3.55e-06), - "bitdeer/deepseek-ai/DeepSeek-V4-Pro": (1.168e-06, 2.336e-06), - "bitdeer/deepseek-ai/DeepSeek-V4-Flash": (4e-08, 8e-08), + "bitdeer-ai/moonshotai/Kimi-K3": (3e-06, 1.5e-05), + "bitdeer-ai/zai-org/GLM-5.2": (1.4e-06, 4.4e-06), + "bitdeer-ai/deepseek-ai/DeepSeek-V4-Flash": (4e-08, 8e-08), } for model, (input_cost, output_cost) in expected_models.items(): assert model in model_cost - assert model_cost[model]["litellm_provider"] == "bitdeer" + assert model_cost[model]["litellm_provider"] == "bitdeer-ai" assert model_cost[model]["input_cost_per_token"] == input_cost assert model_cost[model]["output_cost_per_token"] == output_cost From 64b56efd1bfb88ac66ae44062e6872f230a05281 Mon Sep 17 00:00:00 2001 From: tinatsou Date: Sat, 3 Oct 2026 16:36:09 -0700 Subject: [PATCH 3/7] fix(providers): expose Bitdeer in the Add Model form --- .../provider_create_fields.json | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index 6e96d6ad0ec..f39b2106ba7 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -748,6 +748,34 @@ ], "default_model_placeholder": "gpt-3.5-turbo" }, + { + "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/moonshotai/Kimi-K3" + }, { "provider": "BYTEZ", "provider_display_name": "Bytez", From b774a9c81b772a700247b0a8ee37d56f5584d185 Mon Sep 17 00:00:00 2001 From: kehanjin Date: Tue, 20 Oct 2026 23:35:45 -0700 Subject: [PATCH 4/7] refactor(openai_like): type the JSON provider config fields CHANGE: Annotate the attributes of SimpleProviderConfig with read-only Mapping and Sequence types, so code that reads a JSON provider no longer sees unknown types and the basedpyright error counts drop. base_url stays untyped on purpose, because the api_base None guards in dynamic_config are still reachable in tests SCOPE: litellm/llms/openai_like/json_loader.py --- litellm/llms/openai_like/json_loader.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/litellm/llms/openai_like/json_loader.py b/litellm/llms/openai_like/json_loader.py index 23b63177137..ab0de33712a 100644 --- a/litellm/llms/openai_like/json_loader.py +++ b/litellm/llms/openai_like/json_loader.py @@ -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 {} ) From 45019dfff0289a0a772f2e9171c90894b65b9c03 Mon Sep 17 00:00:00 2001 From: kehanjin Date: Wed, 21 Oct 2026 15:18:12 -0700 Subject: [PATCH 5/7] feat(providers): refresh the Bitdeer chat models to the current catalog CHANGE: Replace the retired Kimi-K3 and GLM-5.2 price rows with the six chat models Bitdeer serves now, at Bitdeer's list prices and with input limits only. DeepSeek-V4-Flash moves from an old discounted price to the list price. The Add Model form defaults to DeepSeek-V4-Flash and the tests use the new model set SCOPE: model_prices_and_context_window*.json, litellm/proxy/public_endpoints/provider_create_fields.json, tests/unit/llms/openai_like/test_bitdeer_provider.py --- ...odel_prices_and_context_window_backup.json | 68 ++++++++++++++----- .../provider_create_fields.json | 2 +- model_prices_and_context_window.json | 68 ++++++++++++++----- .../llms/openai_like/test_bitdeer_provider.py | 13 +++- 4 files changed, 115 insertions(+), 36 deletions(-) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index f9dee70a80a..f9e4b75981a 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -63476,26 +63476,22 @@ ], "supports_audio_output": true }, - "bitdeer-ai/moonshotai/Kimi-K3": { - "max_tokens": 131072, + "bitdeer-ai/deepseek-ai/DeepSeek-V4.1-Flash": { "max_input_tokens": 1048576, - "max_output_tokens": 131072, - "input_cost_per_token": 3e-06, - "output_cost_per_token": 1.5e-05, - "cache_read_input_token_cost": 3e-07, + "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_vision": true, "supports_prompt_caching": true, + "supports_vision": true, "source": "https://www.bitdeer.ai/en/pricing/ai-models" }, - "bitdeer-ai/zai-org/GLM-5.2": { - "max_tokens": 131072, - "max_input_tokens": 1000000, - "max_output_tokens": 131072, + "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, @@ -63507,12 +63503,38 @@ "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_tokens": 384000, - "max_input_tokens": 1000000, - "max_output_tokens": 384000, - "input_cost_per_token": 4e-08, - "output_cost_per_token": 8e-08, + "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", @@ -63522,6 +63544,20 @@ "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" + }, "fallback_generalizations": { "rules": [ { diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index d3f1f6fbb80..48bbca18610 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -820,7 +820,7 @@ "default_value": null } ], - "default_model_placeholder": "bitdeer-ai/moonshotai/Kimi-K3" + "default_model_placeholder": "bitdeer-ai/deepseek-ai/DeepSeek-V4-Flash" }, { "provider": "BYTEZ", diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index f9dee70a80a..f9e4b75981a 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -63476,26 +63476,22 @@ ], "supports_audio_output": true }, - "bitdeer-ai/moonshotai/Kimi-K3": { - "max_tokens": 131072, + "bitdeer-ai/deepseek-ai/DeepSeek-V4.1-Flash": { "max_input_tokens": 1048576, - "max_output_tokens": 131072, - "input_cost_per_token": 3e-06, - "output_cost_per_token": 1.5e-05, - "cache_read_input_token_cost": 3e-07, + "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_vision": true, "supports_prompt_caching": true, + "supports_vision": true, "source": "https://www.bitdeer.ai/en/pricing/ai-models" }, - "bitdeer-ai/zai-org/GLM-5.2": { - "max_tokens": 131072, - "max_input_tokens": 1000000, - "max_output_tokens": 131072, + "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, @@ -63507,12 +63503,38 @@ "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_tokens": 384000, - "max_input_tokens": 1000000, - "max_output_tokens": 384000, - "input_cost_per_token": 4e-08, - "output_cost_per_token": 8e-08, + "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", @@ -63522,6 +63544,20 @@ "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" + }, "fallback_generalizations": { "rules": [ { diff --git a/tests/unit/llms/openai_like/test_bitdeer_provider.py b/tests/unit/llms/openai_like/test_bitdeer_provider.py index 1a0b3ebe9b4..c2346b4157d 100644 --- a/tests/unit/llms/openai_like/test_bitdeer_provider.py +++ b/tests/unit/llms/openai_like/test_bitdeer_provider.py @@ -11,8 +11,15 @@ 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 = "moonshotai/Kimi-K3" -MODELS: Final = (MODEL, "zai-org/GLM-5.2", "deepseek-ai/DeepSeek-V4-Flash") +MODEL: Final = "deepseek-ai/DeepSeek-V4-Flash" +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")) @@ -209,7 +216,7 @@ def test_bitdeer_pricing_and_packaged_registry() -> None: name: Final = f"bitdeer-ai/{model}" entry: Final = prices[name] assert entry == backup[name] == litellm.model_cost[name] - assert entry["max_tokens"] == entry["max_output_tokens"] <= entry["max_input_tokens"] + 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"]) From 0ca8868c39b5c1395f013edb872249581b67d0ea Mon Sep 17 00:00:00 2001 From: kehanjin Date: Tue, 20 Oct 2026 23:37:03 -0700 Subject: [PATCH 6/7] feat(providers): serve Bitdeer embeddings, rerank and image generation CHANGE: JSON providers that declare /v1/embeddings now route embeddings through the OpenAI-compatible handler, and raise if no api_key is resolved so an OpenAI key is never sent to the provider. Rerank for bitdeer-ai reuses the hosted_vllm request and response shape, is priced per token, and refuses to run without a Bitdeer key. Adds the bge-m3, bge-reranker-v2-m3 and seedream-5.0-lite models and marks the three endpoints as supported SCOPE: litellm/main.py, litellm/utils.py, litellm/rerank_api/main.py, litellm/llms/openai_like/*, model_prices_and_context_window*.json, provider_endpoints_support*.json, tests/unit/llms/openai_like/test_bitdeer_provider.py --- litellm/llms/openai_like/json_loader.py | 5 + litellm/llms/openai_like/providers.json | 2 +- .../rerank/bitdeer_transformation.py | 22 ++++ litellm/main.py | 21 +++ ...odel_prices_and_context_window_backup.json | 26 ++++ .../provider_endpoints_support_backup.json | 6 +- litellm/rerank_api/main.py | 22 ++++ litellm/utils.py | 8 +- model_prices_and_context_window.json | 26 ++++ provider_endpoints_support.json | 6 +- .../llms/openai_like/test_bitdeer_provider.py | 120 +++++++++++++++++- 11 files changed, 255 insertions(+), 9 deletions(-) create mode 100644 litellm/llms/openai_like/rerank/bitdeer_transformation.py diff --git a/litellm/llms/openai_like/json_loader.py b/litellm/llms/openai_like/json_loader.py index ab0de33712a..11f753252cc 100644 --- a/litellm/llms/openai_like/json_loader.py +++ b/litellm/llms/openai_like/json_loader.py @@ -87,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""" diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index 0ca99e16693..d397e53d559 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -8,7 +8,7 @@ "special_handling": { "require_api_key_for_custom_base": true }, - "supported_endpoints": ["/v1/chat/completions"] + "supported_endpoints": ["/v1/chat/completions", "/v1/embeddings", "/v1/rerank", "/v1/images/generations"] }, "publicai": { "base_url": "https://api.publicai.co/v1", diff --git a/litellm/llms/openai_like/rerank/bitdeer_transformation.py b/litellm/llms/openai_like/rerank/bitdeer_transformation.py new file mode 100644 index 00000000000..991c4fc5e52 --- /dev/null +++ b/litellm/llms/openai_like/rerank/bitdeer_transformation.py @@ -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 diff --git a/litellm/main.py b/litellm/main.py index 17f9cebe43f..73ac5ffed92 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -7652,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): diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index f9e4b75981a..9782e0149f8 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -63558,6 +63558,32 @@ "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": [ { diff --git a/litellm/provider_endpoints_support_backup.json b/litellm/provider_endpoints_support_backup.json index c76aaf34646..ac8a2e0cc24 100644 --- a/litellm/provider_endpoints_support_backup.json +++ b/litellm/provider_endpoints_support_backup.json @@ -39,13 +39,13 @@ "chat_completions": true, "messages": true, "responses": true, - "embeddings": false, - "image_generations": false, + "embeddings": true, + "image_generations": true, "audio_transcriptions": false, "audio_speech": false, "moderations": false, "batches": false, - "rerank": false, + "rerank": true, "a2a": false } }, diff --git a/litellm/rerank_api/main.py b/litellm/rerank_api/main.py index f7e2616324f..5dc4e391a7d 100644 --- a/litellm/rerank_api/main.py +++ b/litellm/rerank_api/main.py @@ -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") diff --git a/litellm/utils.py b/litellm/utils.py index 7de7ce6188d..5f8fe0f72f4 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -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: diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index f9e4b75981a..9782e0149f8 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -63558,6 +63558,32 @@ "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": [ { diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 8adeac16905..fcfe71e89ec 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -40,13 +40,13 @@ "chat_completions": true, "messages": true, "responses": true, - "embeddings": false, - "image_generations": false, + "embeddings": true, + "image_generations": true, "audio_transcriptions": false, "audio_speech": false, "moderations": false, "batches": false, - "rerank": false, + "rerank": true, "a2a": false } }, diff --git a/tests/unit/llms/openai_like/test_bitdeer_provider.py b/tests/unit/llms/openai_like/test_bitdeer_provider.py index c2346b4157d..b8963da89b0 100644 --- a/tests/unit/llms/openai_like/test_bitdeer_provider.py +++ b/tests/unit/llms/openai_like/test_bitdeer_provider.py @@ -12,6 +12,9 @@ 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", @@ -212,12 +215,127 @@ def test_bitdeer_pricing_and_packaged_registry() -> None: ) 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 == backup[name] == litellm.model_cost[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"] + ) From 4a1f465c52d5c0c3c37f317244e5b14d99d6b52d Mon Sep 17 00:00:00 2001 From: kehanjin Date: Wed, 21 Oct 2026 15:52:28 -0700 Subject: [PATCH 7/7] test(providers): satisfy ruff PT012 in the Bitdeer key guard tests CHANGE: Move the chat and image dispatch out of the pytest.raises blocks into small helpers so each block holds one statement, which the tests ruff config requires. The tests assert the same things as before. The unique-entries helper now returns a read-only Mapping SCOPE: tests/unit/llms/openai_like/test_bitdeer_provider.py --- .../llms/openai_like/test_bitdeer_provider.py | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/tests/unit/llms/openai_like/test_bitdeer_provider.py b/tests/unit/llms/openai_like/test_bitdeer_provider.py index b8963da89b0..a311a89df25 100644 --- a/tests/unit/llms/openai_like/test_bitdeer_provider.py +++ b/tests/unit/llms/openai_like/test_bitdeer_provider.py @@ -1,4 +1,5 @@ import json +from collections.abc import Mapping, Sequence from pathlib import Path from typing import Final, Literal @@ -151,6 +152,27 @@ def test_bitdeer_completion_rejects_custom_base_before_sending(monkeypatch: pyte 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( @@ -168,15 +190,7 @@ def test_bitdeer_missing_credentials_never_send_openai_key( with respx.mock() as upstream: with pytest.raises(litellm.AuthenticationError, match="BITDEER_API_KEY") as error: - if endpoint == "chat": - litellm.completion( - model=model, - custom_llm_provider=provider, - api_base=api_base, - messages=[{"role": "user", "content": "Hi"}], - ) - else: - litellm.image_generation(model=model, custom_llm_provider=provider, api_base=api_base, prompt="A tree") + _request(endpoint, model=model, custom_llm_provider=provider, api_base=api_base) assert len(upstream.calls) == 0 assert "secret" not in str(error.value) @@ -194,15 +208,12 @@ async def test_bitdeer_async_missing_credentials_never_send_openai_key( with respx.mock() as upstream: with pytest.raises(litellm.AuthenticationError, match="BITDEER_API_KEY") as error: - if endpoint == "chat": - await litellm.acompletion(model=f"bitdeer-ai/{MODEL}", messages=[{"role": "user", "content": "Hi"}]) - else: - await litellm.aimage_generation(model=f"bitdeer-ai/{MODEL}", prompt="A tree") + await _arequest(endpoint, model=f"bitdeer-ai/{MODEL}") assert len(upstream.calls) == 0 assert "secret" not in str(error.value) -def _unique_entries(pairs: list[tuple[str, object]]) -> dict[str, object]: +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)