Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions litellm/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,7 @@
"https://api.libertai.io/v1",
"https://pinstripes.io/v1",
"https://api.meta.ai/v1",
"https://api-inference.bitdeer.ai/v1",
]


Expand Down Expand Up @@ -821,6 +822,7 @@
"pinstripes", # Pinstripes - JSON-configured provider
"darkbloom",
"meta", # Meta Model API (Muse Spark) - JSON-configured provider
"bitdeer-ai", # Bitdeer AI - JSON-configured provider
]
openai_text_completion_compatible_providers: Final[list] = [ # providers that support `/v1/completions`
"together_ai",
Expand Down
3 changes: 3 additions & 0 deletions litellm/litellm_core_utils/get_llm_provider_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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-ai"
dynamic_api_key = get_secret_str("BITDEER_API_KEY")
Comment on lines +328 to +330

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Explicit API key gets overwritten

If BITDEER_API_KEY is set, URL autodetection replaces the caller's explicit api_key with that environment value, causing authentication to use the wrong Bitdeer account or fail despite a valid explicit credential.

Suggested change
elif endpoint == "https://api-inference.bitdeer.ai/v1":
custom_llm_provider = "bitdeer-ai"
dynamic_api_key = get_secret_str("BITDEER_API_KEY")
elif endpoint == "https://api-inference.bitdeer.ai/v1":
custom_llm_provider = "bitdeer-ai"
dynamic_api_key = api_key or get_secret_str("BITDEER_API_KEY")

Knowledge Base Used: LLM Provider Adapters

elif endpoint == "https://api.v0.dev/v1":
custom_llm_provider = "v0"
dynamic_api_key = get_secret_str("V0_API_KEY")
Expand Down
7 changes: 7 additions & 0 deletions litellm/llms/openai_like/providers.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,5 +183,12 @@
"max_completion_tokens": "max_tokens"
},
"supported_endpoints": ["/v1/chat/completions", "/v1/responses", "/v1/embeddings"]
},
"bitdeer-ai": {
"base_url": "https://api-inference.bitdeer.ai/v1",
"api_key_env": "BITDEER_API_KEY",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: API key exfiltration through an overridden base URL

A caller can submit model: "bitdeer-ai/..." with api_base pointing to a server they control and omit api_key. The JSON-provider resolver preserves that api_base but falls back to BITDEER_API_KEY, so the outbound request exposes the server's credential in its Authorization header. Reject untrusted base-URL overrides for this provider, or require an explicit request API key whenever the selected base URL differs from the configured Bitdeer URL.

"param_mappings": {
"max_completion_tokens": "max_tokens"
}
}
}
1 change: 1 addition & 0 deletions litellm/types/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3710,6 +3710,7 @@ class LlmProviders(str, Enum):
XIAOMI_MIMO = "xiaomi_mimo"
TENSORMESH = "tensormesh"
LIBERTAI = "libertai"
BITDEER_AI = "bitdeer-ai"
PINSTRIPES = "pinstripes"
DARKBLOOM = "darkbloom"
META = "meta"
Expand Down
50 changes: 50 additions & 0 deletions model_prices_and_context_window.json
Original file line number Diff line number Diff line change
Expand Up @@ -47176,6 +47176,56 @@
],
"supports_audio_output": true
},
"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-ai",
"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-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-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-Flash": {
"supports_prompt_caching": true,
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
},
Comment on lines +47210 to +47213

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicate model pricing key

bitdeer-ai/deepseek-ai/DeepSeek-V4-Flash is declared twice, so standard JSON loading silently discards the first capability record and existing schema and pricing tests cannot detect edits made to it.

Suggested change
"bitdeer-ai/deepseek-ai/DeepSeek-V4-Flash": {
"supports_prompt_caching": true,
"source": "https://www.bitdeer.ai/en/pricing/ai-models"
},

Knowledge Base Used: Cost Tracking and Budget Enforcement

"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-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"
},
"fallback_generalizations": {
"rules": [
{
Expand Down
120 changes: 120 additions & 0 deletions tests/test_litellm/llms/openai_like/test_bitdeer_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""
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_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-ai")

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-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-ai/moonshotai/Kimi-K3",
custom_llm_provider=None,
api_base=None,
api_key=None,
)

assert model == "moonshotai/Kimi-K3"
assert provider == "bitdeer-ai"
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-ai/moonshotai/Kimi-K3",
custom_llm_provider=None,
api_base="https://custom.bitdeer.ai/v1",
api_key="sk-test",
)

assert provider == "bitdeer-ai"
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-ai"
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-ai-kimi-k3",
"litellm_params": {
"model": "bitdeer-ai/moonshotai/Kimi-K3",
"api_key": "test-key",
},
}
]
)

assert len(router.model_list) == 1
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"""
with open(
os.path.join(workspace_path, "model_prices_and_context_window.json")
) as f:
model_cost = json.load(f)

expected_models = {
"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-ai"
assert model_cost[model]["input_cost_per_token"] == input_cost
assert model_cost[model]["output_cost_per_token"] == output_cost
Loading