mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-12 08:42:18 +00:00
Merge b7b401eda5 into f4a2e2b3af
This commit is contained in:
commit
cafe31faaf
5 changed files with 146 additions and 0 deletions
|
|
@ -79,6 +79,17 @@ def create_config_class(provider: SimpleProviderConfig):
|
|||
# Resolve API key
|
||||
resolved_key: Final = api_key or get_secret_str(provider.api_key_env)
|
||||
|
||||
if provider.special_handling.get("require_api_key") and not resolved_key:
|
||||
# Fail closed: without this the caller's OPENAI_API_KEY would be used as a fallback
|
||||
# further down the dispatch path and sent to this provider.
|
||||
from litellm.exceptions import AuthenticationError
|
||||
|
||||
raise AuthenticationError(
|
||||
message=f"{provider.slug}: no API key. Pass api_key or set {provider.api_key_env}.",
|
||||
llm_provider=provider.slug,
|
||||
model="",
|
||||
)
|
||||
|
||||
return resolved_base, resolved_key
|
||||
|
||||
def get_openai_compatible_provider_info(
|
||||
|
|
|
|||
|
|
@ -302,5 +302,15 @@
|
|||
"preserve_cache_control": true
|
||||
},
|
||||
"supported_endpoints": ["/v1/chat/completions"]
|
||||
},
|
||||
"sagg": {
|
||||
"base_url": "https://api.privatedeskai.com/v1",
|
||||
"api_key_env": "SAGG_API_KEY",
|
||||
"param_mappings": {
|
||||
"max_completion_tokens": "max_tokens"
|
||||
},
|
||||
"special_handling": {
|
||||
"require_api_key": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4709,6 +4709,7 @@ class LlmProviders(str, Enum):
|
|||
HOSTINGER_ROUTER = "hostinger_router"
|
||||
ZEROGPU = "zerogpu"
|
||||
DARKBLOOM = "darkbloom"
|
||||
SAGG = "sagg"
|
||||
META = "meta"
|
||||
SAIL = "sail"
|
||||
REKA = "reka"
|
||||
|
|
|
|||
|
|
@ -3578,6 +3578,22 @@
|
|||
"generateContent": false
|
||||
}
|
||||
},
|
||||
"sagg": {
|
||||
"display_name": "SAGG (`sagg`)",
|
||||
"url": "https://docs.litellm.ai/docs/providers/sagg",
|
||||
"endpoints": {
|
||||
"chat_completions": true,
|
||||
"messages": false,
|
||||
"responses": false,
|
||||
"embeddings": false,
|
||||
"image_generations": false,
|
||||
"audio_transcriptions": false,
|
||||
"audio_speech": false,
|
||||
"moderations": false,
|
||||
"batches": false,
|
||||
"rerank": false
|
||||
}
|
||||
},
|
||||
"searxng": {
|
||||
"display_name": "SearXNG (`searxng`)",
|
||||
"url": "https://docs.litellm.ai/docs/search/searxng",
|
||||
|
|
|
|||
108
tests/unit/llms/openai_like/test_sagg_provider.py
Normal file
108
tests/unit/llms/openai_like/test_sagg_provider.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
"""
|
||||
Tests for SAGG provider configuration and integration.
|
||||
|
||||
SAGG (https://api.privatedeskai.com) is an OpenAI-compatible LLM
|
||||
inference gateway with automatic multi-provider failover.
|
||||
"""
|
||||
|
||||
import litellm
|
||||
|
||||
|
||||
class TestSaggProviderConfig:
|
||||
def test_sagg_in_provider_list(self):
|
||||
from litellm import LlmProviders
|
||||
|
||||
assert hasattr(LlmProviders, "SAGG")
|
||||
assert LlmProviders.SAGG.value == "sagg"
|
||||
assert "sagg" in litellm.provider_list
|
||||
|
||||
def test_sagg_json_config_exists(self):
|
||||
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
|
||||
|
||||
assert JSONProviderRegistry.exists("sagg")
|
||||
|
||||
sagg = JSONProviderRegistry.get("sagg")
|
||||
assert sagg is not None
|
||||
assert sagg.base_url == "https://api.privatedeskai.com/v1"
|
||||
assert sagg.api_key_env == "SAGG_API_KEY"
|
||||
assert sagg.param_mappings.get("max_completion_tokens") == "max_tokens"
|
||||
|
||||
def test_sagg_provider_resolution(self):
|
||||
"""The real model id contains its own slash
|
||||
(deepseek-ai/DeepSeek-V4-Flash-0731), so this also confirms
|
||||
provider-prefix splitting only splits on the FIRST slash, same
|
||||
as e.g. pinstripes/ps/glm-4.5-air elsewhere in this registry."""
|
||||
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
|
||||
|
||||
model, provider, api_key, api_base = get_llm_provider(
|
||||
model="sagg/deepseek-ai/DeepSeek-V4-Flash-0731",
|
||||
custom_llm_provider=None,
|
||||
api_base=None,
|
||||
api_key="test-key",
|
||||
)
|
||||
|
||||
assert model == "deepseek-ai/DeepSeek-V4-Flash-0731"
|
||||
assert provider == "sagg"
|
||||
assert api_base == "https://api.privatedeskai.com/v1"
|
||||
|
||||
def test_sagg_router_config(self):
|
||||
from litellm import Router
|
||||
|
||||
router = Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "sagg-deepseek",
|
||||
"litellm_params": {
|
||||
"model": "sagg/deepseek-ai/DeepSeek-V4-Flash-0731",
|
||||
"api_key": "test-key",
|
||||
},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
assert len(router.model_list) == 1
|
||||
assert router.model_list[0]["model_name"] == "sagg-deepseek"
|
||||
|
||||
def test_sagg_parameter_mapping(self):
|
||||
"""SAGG's own request parser only recognizes max_tokens, so a
|
||||
caller sending max_completion_tokens needs this mapping to
|
||||
actually take effect server-side rather than being dropped."""
|
||||
from litellm.llms.openai_like.dynamic_config import create_config_class
|
||||
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
|
||||
|
||||
provider = JSONProviderRegistry.get("sagg")
|
||||
config_class = create_config_class(provider)
|
||||
config = config_class()
|
||||
|
||||
optional_params = {}
|
||||
non_default_params = {"max_completion_tokens": 100, "temperature": 0.7}
|
||||
result = config.map_openai_params(
|
||||
non_default_params, optional_params, "deepseek-ai/DeepSeek-V4-Flash-0731", False
|
||||
)
|
||||
|
||||
assert "max_tokens" in result
|
||||
assert result["max_tokens"] == 100
|
||||
assert "max_completion_tokens" not in result
|
||||
assert result["temperature"] == 0.7
|
||||
|
||||
|
||||
class TestSaggRequiresItsOwnKey:
|
||||
def _config(self):
|
||||
from litellm.llms.openai_like.dynamic_config import create_config_class
|
||||
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
|
||||
|
||||
return create_config_class(JSONProviderRegistry.get("sagg"))()
|
||||
|
||||
def test_missing_key_fails_closed_even_with_openai_key_set(self, monkeypatch):
|
||||
import pytest
|
||||
|
||||
monkeypatch.delenv("SAGG_API_KEY", raising=False)
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "sk-not-for-sagg")
|
||||
with pytest.raises(litellm.AuthenticationError):
|
||||
self._config()._get_openai_compatible_provider_info(None, None)
|
||||
|
||||
def test_explicit_key_and_env_key_still_work(self, monkeypatch):
|
||||
monkeypatch.delenv("SAGG_API_KEY", raising=False)
|
||||
assert self._config()._get_openai_compatible_provider_info(None, "k1")[1] == "k1"
|
||||
monkeypatch.setenv("SAGG_API_KEY", "k2")
|
||||
assert self._config()._get_openai_compatible_provider_info(None, None)[1] == "k2"
|
||||
Loading…
Add table
Reference in a new issue