mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-12 03:48:56 +00:00
Merge 64b56efd1b into 950da7c2ec
This commit is contained in:
commit
cefb60f920
11 changed files with 347 additions and 0 deletions
|
|
@ -965,6 +965,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",
|
||||
"https://api.sailresearch.com/v1",
|
||||
"https://api.cognition.ai/v1",
|
||||
"https://api.cortecs.ai/v1",
|
||||
|
|
@ -1042,6 +1043,7 @@ openai_compatible_providers: Final[list] = [
|
|||
"pinstripes", # Pinstripes - JSON-configured provider
|
||||
"darkbloom",
|
||||
"meta", # Meta Model API (Muse Spark) - JSON-configured provider
|
||||
"bitdeer-ai", # Bitdeer AI - JSON-configured provider
|
||||
"cognition",
|
||||
"cortecs",
|
||||
"scx-ai",
|
||||
|
|
|
|||
|
|
@ -371,6 +371,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")
|
||||
|
|
|
|||
|
|
@ -66,6 +66,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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
},
|
||||
"publicai": {
|
||||
"base_url": "https://api.publicai.co/v1",
|
||||
"api_key_env": "PUBLICAI_API_KEY",
|
||||
|
|
|
|||
|
|
@ -61225,6 +61225,52 @@
|
|||
],
|
||||
"supports_audio_output": true
|
||||
},
|
||||
"bitdeer-ai/moonshotai/Kimi-K3": {
|
||||
"max_tokens": 131072,
|
||||
"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": 131072,
|
||||
"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": {
|
||||
"max_tokens": 384000,
|
||||
"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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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": false,
|
||||
"image_generations": false,
|
||||
"audio_transcriptions": false,
|
||||
"audio_speech": false,
|
||||
"moderations": false,
|
||||
"batches": false,
|
||||
"rerank": false,
|
||||
"a2a": false
|
||||
}
|
||||
},
|
||||
"a2a": {
|
||||
"display_name": "A2A (Agent-to-Agent) (`a2a`)",
|
||||
"url": "https://docs.litellm.ai/docs/providers/a2a",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -4200,6 +4200,7 @@ class LlmProviders(str, Enum):
|
|||
XIAOMI_MIMO = "xiaomi_mimo"
|
||||
TENSORMESH = "tensormesh"
|
||||
LIBERTAI = "libertai"
|
||||
BITDEER_AI = "bitdeer-ai"
|
||||
PINSTRIPES = "pinstripes"
|
||||
COGNITION = "cognition"
|
||||
CORTECS = "cortecs"
|
||||
|
|
|
|||
|
|
@ -61225,6 +61225,52 @@
|
|||
],
|
||||
"supports_audio_output": true
|
||||
},
|
||||
"bitdeer-ai/moonshotai/Kimi-K3": {
|
||||
"max_tokens": 131072,
|
||||
"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": 131072,
|
||||
"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": {
|
||||
"max_tokens": 384000,
|
||||
"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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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": false,
|
||||
"image_generations": false,
|
||||
"audio_transcriptions": false,
|
||||
"audio_speech": false,
|
||||
"moderations": false,
|
||||
"batches": false,
|
||||
"rerank": false,
|
||||
"a2a": false
|
||||
}
|
||||
},
|
||||
"a2a": {
|
||||
"display_name": "A2A (Agent-to-Agent) (`a2a`)",
|
||||
"url": "https://docs.litellm.ai/docs/providers/a2a",
|
||||
|
|
|
|||
164
tests/unit/llms/openai_like/test_bitdeer_provider.py
Normal file
164
tests/unit/llms/openai_like/test_bitdeer_provider.py
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
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 = "moonshotai/Kimi-K3"
|
||||
MODELS: Final = (MODEL, "zai-org/GLM-5.2", "deepseek-ai/DeepSeek-V4-Flash")
|
||||
|
||||
|
||||
@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 _unique_entries(pairs: list[tuple[str, object]]) -> dict[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:
|
||||
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["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"])
|
||||
Loading…
Add table
Reference in a new issue