This commit is contained in:
Ruijie 2026-10-21 22:54:03 +00:00 • committed by GitHub
commit 12ca544f51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 755 additions and 7 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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"]
)