From 9d148c4670d01cd9e879aeb4f8ae96317e05d698 Mon Sep 17 00:00:00 2001 From: privatedeskai Date: Mon, 31 Aug 2026 19:11:20 -0700 Subject: [PATCH 1/3] feat(providers): add SAGG as an OpenAI-compatible provider SAGG (https://api.privatedeskai.com) is an OpenAI-compatible LLM inference gateway with automatic multi-provider failover. Adds it as a dynamic JSON-registry provider: base_url + api_key_env in providers.json, plus the required LlmProviders enum entry so it resolves via get_llm_provider() and appears in litellm.provider_list. param_mappings.max_completion_tokens -> max_tokens is required, not optional: SAGG's own request parser only recognizes max_tokens (has no max_completion_tokens field at all), so a caller using the newer OpenAI param name would otherwise have it silently dropped. Verified end-to-end against the live SAGG API, including that the max_completion_tokens mapping is actually enforced server-side (finish_reason: length at the requested cap), not just unit-tested. --- litellm/llms/openai_like/providers.json | 7 + litellm/types/utils.py | 1 + .../llms/openai_like/test_sagg_provider.py | 187 ++++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 tests/test_litellm/llms/openai_like/test_sagg_provider.py diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index a458a209ea9..7a313a2a773 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -200,5 +200,12 @@ "temperature_max": 1.99 }, "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" + } } } diff --git a/litellm/types/utils.py b/litellm/types/utils.py index a1b3523442b..a18df800292 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3857,6 +3857,7 @@ class LlmProviders(str, Enum): COGNITION = "cognition" SCX_AI = "scx-ai" DARKBLOOM = "darkbloom" + SAGG = "sagg" META = "meta" LITELLM_AGENT = "litellm_agent" CURSOR = "cursor" diff --git a/tests/test_litellm/llms/openai_like/test_sagg_provider.py b/tests/test_litellm/llms/openai_like/test_sagg_provider.py new file mode 100644 index 00000000000..b36b7a61135 --- /dev/null +++ b/tests/test_litellm/llms/openai_like/test_sagg_provider.py @@ -0,0 +1,187 @@ +""" +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 os +import sys +from unittest.mock import MagicMock, patch + +try: + import pytest +except ImportError: + pytest = None + +# Add workspace to path +workspace_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../..")) +sys.path.insert(0, workspace_path) + +import litellm + + +class TestSaggProviderConfig: + """Test SAGG provider configuration""" + + def test_sagg_in_provider_list(self): + """Test that sagg is in the provider list""" + from litellm import LlmProviders + + # Verify sagg is in the enum + assert hasattr(LlmProviders, "SAGG") + assert LlmProviders.SAGG.value == "sagg" + + # Verify it's in the provider list + assert "sagg" in litellm.provider_list + + def test_sagg_json_config_exists(self): + """Test that sagg is configured in providers.json""" + from litellm.llms.openai_like.json_loader import JSONProviderRegistry + + # Verify sagg is loaded + assert JSONProviderRegistry.exists("sagg") + + # Get sagg config + 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): + """Test that provider resolution finds sagg - 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 same 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=None, + ) + + 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): + """Test that sagg can be used in Router configuration""" + from litellm import Router + + # This should not raise "Unsupported provider - sagg" + router = Router( + model_list=[ + { + "model_name": "sagg-deepseek", + "litellm_params": { + "model": "sagg/deepseek-ai/DeepSeek-V4-Flash-0731", + "api_key": "test-key", + }, + } + ] + ) + + # Verify the deployment was created successfully + assert len(router.model_list) == 1 + assert router.model_list[0]["model_name"] == "sagg-deepseek" + + def test_sagg_parameter_mapping(self): + """Test that max_completion_tokens is mapped to max_tokens for + sagg - SAGG's own request parser only recognizes max_tokens + (cmd/gateway/billing.go's chatRequestForBilling struct has no + max_completion_tokens field at all), so a caller sending the + newer OpenAI param name needs this mapping to actually take + effect server-side, not be silently 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 TestSaggIntegration: + """Integration tests for SAGG provider""" + + def test_sagg_completion_basic(self): + """Test basic completion call to SAGG""" + # Skip test if API key not set in environment + if not os.environ.get("SAGG_API_KEY"): + if pytest: + pytest.skip("SAGG_API_KEY not set") + return + + try: + response = litellm.completion( + model="sagg/deepseek-ai/DeepSeek-V4-Flash-0731", + messages=[ + { + "role": "user", + "content": "Say 'test successful' and nothing else", + } + ], + max_tokens=10, + ) + + assert response is not None + assert hasattr(response, "choices") + assert len(response.choices) > 0 + assert hasattr(response.choices[0], "message") + assert hasattr(response.choices[0].message, "content") + assert response.choices[0].message.content is not None + + content = response.choices[0].message.content.lower() + assert len(content) > 0 + + print(f"✓ SAGG completion successful: {response.choices[0].message.content}") + + except Exception as e: + if pytest: + pytest.fail(f"SAGG completion failed: {str(e)}") + else: + raise + + +if __name__ == "__main__": + print("Testing SAGG Provider...") + + test_config = TestSaggProviderConfig() + + print("\n1. Testing provider in list...") + test_config.test_sagg_in_provider_list() + print(" ✓ sagg in provider list") + + print("\n2. Testing JSON config...") + test_config.test_sagg_json_config_exists() + print(" ✓ sagg JSON config loaded") + + print("\n3. Testing provider resolution...") + test_config.test_sagg_provider_resolution() + print(" ✓ Provider resolution works") + + print("\n4. Testing router configuration...") + test_config.test_sagg_router_config() + print(" ✓ Router configuration works") + + print("\n5. Testing parameter mapping...") + test_config.test_sagg_parameter_mapping() + print(" ✓ Parameter mapping works") + + print("\n" + "=" * 50) + print("✓ All configuration tests passed!") + print("=" * 50) From b876e2b04d545aeaa75d662658212110a846bc36 Mon Sep 17 00:00:00 2001 From: privatedeskai Date: Wed, 2 Sep 2026 09:31:29 -0700 Subject: [PATCH 2/3] fix(providers): address Greptile review on SAGG test file Remove unused MagicMock/patch imports (F401, blocking required lint checks). Remove the live-network completion test: tests/e2e/CLAUDE.md's Hard Rules forbid substituting a unit test for e2e feature coverage, and this env-var-gated call doesn't fit the e2e harness either (no proxy, no ProxyClient, no coverage-registry marker) - a proper e2e addition is a separate, larger piece of work, out of scope here. The real live proof stays in the PR description's Proof of Fix section, where CLAUDE.md says it belongs. Trim docstrings and inline comments that only restated the assertion on the next line, per CLAUDE.md's comment policy - kept the two that explain non-obvious behavior (first-slash-only provider splitting, why the param mapping matters). --- .../llms/openai_like/test_sagg_provider.py | 78 ++----------------- 1 file changed, 7 insertions(+), 71 deletions(-) diff --git a/tests/test_litellm/llms/openai_like/test_sagg_provider.py b/tests/test_litellm/llms/openai_like/test_sagg_provider.py index b36b7a61135..7eef238c02b 100644 --- a/tests/test_litellm/llms/openai_like/test_sagg_provider.py +++ b/tests/test_litellm/llms/openai_like/test_sagg_provider.py @@ -7,12 +7,6 @@ inference gateway with automatic multi-provider failover. import os import sys -from unittest.mock import MagicMock, patch - -try: - import pytest -except ImportError: - pytest = None # Add workspace to path workspace_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../..")) @@ -22,27 +16,18 @@ import litellm class TestSaggProviderConfig: - """Test SAGG provider configuration""" - def test_sagg_in_provider_list(self): - """Test that sagg is in the provider list""" from litellm import LlmProviders - # Verify sagg is in the enum assert hasattr(LlmProviders, "SAGG") assert LlmProviders.SAGG.value == "sagg" - - # Verify it's in the provider list assert "sagg" in litellm.provider_list def test_sagg_json_config_exists(self): - """Test that sagg is configured in providers.json""" from litellm.llms.openai_like.json_loader import JSONProviderRegistry - # Verify sagg is loaded assert JSONProviderRegistry.exists("sagg") - # Get sagg config sagg = JSONProviderRegistry.get("sagg") assert sagg is not None assert sagg.base_url == "https://api.privatedeskai.com/v1" @@ -50,11 +35,10 @@ class TestSaggProviderConfig: assert sagg.param_mappings.get("max_completion_tokens") == "max_tokens" def test_sagg_provider_resolution(self): - """Test that provider resolution finds sagg - 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 same registry.""" + """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( @@ -69,10 +53,8 @@ class TestSaggProviderConfig: assert api_base == "https://api.privatedeskai.com/v1" def test_sagg_router_config(self): - """Test that sagg can be used in Router configuration""" from litellm import Router - # This should not raise "Unsupported provider - sagg" router = Router( model_list=[ { @@ -85,17 +67,13 @@ class TestSaggProviderConfig: ] ) - # Verify the deployment was created successfully assert len(router.model_list) == 1 assert router.model_list[0]["model_name"] == "sagg-deepseek" def test_sagg_parameter_mapping(self): - """Test that max_completion_tokens is mapped to max_tokens for - sagg - SAGG's own request parser only recognizes max_tokens - (cmd/gateway/billing.go's chatRequestForBilling struct has no - max_completion_tokens field at all), so a caller sending the - newer OpenAI param name needs this mapping to actually take - effect server-side, not be silently dropped.""" + """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 @@ -115,48 +93,6 @@ class TestSaggProviderConfig: assert result["temperature"] == 0.7 -class TestSaggIntegration: - """Integration tests for SAGG provider""" - - def test_sagg_completion_basic(self): - """Test basic completion call to SAGG""" - # Skip test if API key not set in environment - if not os.environ.get("SAGG_API_KEY"): - if pytest: - pytest.skip("SAGG_API_KEY not set") - return - - try: - response = litellm.completion( - model="sagg/deepseek-ai/DeepSeek-V4-Flash-0731", - messages=[ - { - "role": "user", - "content": "Say 'test successful' and nothing else", - } - ], - max_tokens=10, - ) - - assert response is not None - assert hasattr(response, "choices") - assert len(response.choices) > 0 - assert hasattr(response.choices[0], "message") - assert hasattr(response.choices[0].message, "content") - assert response.choices[0].message.content is not None - - content = response.choices[0].message.content.lower() - assert len(content) > 0 - - print(f"✓ SAGG completion successful: {response.choices[0].message.content}") - - except Exception as e: - if pytest: - pytest.fail(f"SAGG completion failed: {str(e)}") - else: - raise - - if __name__ == "__main__": print("Testing SAGG Provider...") From 5e09b3ffcbd59836632cb3839492c691b86ed3d4 Mon Sep 17 00:00:00 2001 From: privatedeskai Date: Wed, 2 Sep 2026 14:26:34 -0700 Subject: [PATCH 3/3] fix(providers): fix the 3 CI checks still failing on SAGG PR lint: the real cause was never F401 (that step already passed) - it was scripts/test_quality_gate.py's TQ003 rule, which flags sys.path.insert as unnecessary since pytest's rootdir handling and the installed package already make litellm importable. Removed it along with the now-unneeded os/sys imports and the __main__ block that depended on it, matching test_scx_ai_provider.py's cleaner convention. Confirmed directly: scripts/check_test_quality.py now reports 0 violations on this file (it still reports 2 on test_xiaomi_mimo.py, the template this was built from - grandfathered only because it predates the gate). code-quality: check_provider_folders_documented.py requires every litellm/llms/openai_like/providers.json entry to have a matching entry in provider_endpoints_support.json. sagg had none. Added one, chat_completions only (the only endpoint actually verified in this PR's own proof of fix) - confirmed the check now passes, and fails again with the exact expected error when the entry is removed. osv-scan: not caused by this PR. Zero dependency/lockfile files are touched anywhere in this diff. Cross-checked two other open, unrelated PRs against the same base branch: one also failed osv-scan in the same time window this PR's check ran, the other two (scanned ~4.5 hours later) both passed - a transient, time-bound, repo-wide/base-branch issue, not something this PR introduced or can fix. --- provider_endpoints_support.json | 16 ++++++++ .../llms/openai_like/test_sagg_provider.py | 37 ------------------- 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 7c7d508856f..feb10c8622e 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -2191,6 +2191,22 @@ "interactions": true } }, + "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", diff --git a/tests/test_litellm/llms/openai_like/test_sagg_provider.py b/tests/test_litellm/llms/openai_like/test_sagg_provider.py index 7eef238c02b..da76601da56 100644 --- a/tests/test_litellm/llms/openai_like/test_sagg_provider.py +++ b/tests/test_litellm/llms/openai_like/test_sagg_provider.py @@ -5,13 +5,6 @@ SAGG (https://api.privatedeskai.com) is an OpenAI-compatible LLM inference gateway with automatic multi-provider failover. """ -import os -import sys - -# Add workspace to path -workspace_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../..")) -sys.path.insert(0, workspace_path) - import litellm @@ -91,33 +84,3 @@ class TestSaggProviderConfig: assert result["max_tokens"] == 100 assert "max_completion_tokens" not in result assert result["temperature"] == 0.7 - - -if __name__ == "__main__": - print("Testing SAGG Provider...") - - test_config = TestSaggProviderConfig() - - print("\n1. Testing provider in list...") - test_config.test_sagg_in_provider_list() - print(" ✓ sagg in provider list") - - print("\n2. Testing JSON config...") - test_config.test_sagg_json_config_exists() - print(" ✓ sagg JSON config loaded") - - print("\n3. Testing provider resolution...") - test_config.test_sagg_provider_resolution() - print(" ✓ Provider resolution works") - - print("\n4. Testing router configuration...") - test_config.test_sagg_router_config() - print(" ✓ Router configuration works") - - print("\n5. Testing parameter mapping...") - test_config.test_sagg_parameter_mapping() - print(" ✓ Parameter mapping works") - - print("\n" + "=" * 50) - print("✓ All configuration tests passed!") - print("=" * 50)