diff --git a/README.md b/README.md index 7ffc44854bb..85e06ded952 100644 --- a/README.md +++ b/README.md @@ -399,6 +399,7 @@ Set `LITELLM_PROXY_API_BASE` and `LITELLM_PROXY_API_KEY` and every model call th | [Typesafe Decisions API (`typesafe`)](https://docs.litellm.ai/docs/providers) | | | | | | | | | | | | [V0 (`v0`)](https://docs.litellm.ai/docs/providers/v0) | ✅ | ✅ | ✅ | | | | | | | | | [Vercel AI Gateway (`vercel_ai_gateway`)](https://docs.litellm.ai/docs/providers/vercel_ai_gateway) | ✅ | ✅ | ✅ | | | | | | | | +| [Viktor (`viktor`)](https://docs.litellm.ai/docs/providers/viktor) | ✅ | | ✅ | | | | | | | | | [VLLM (`vllm`)](https://docs.litellm.ai/docs/providers/vllm) | ✅ | ✅ | ✅ | | | | | | | | | [Volcengine (`volcengine`)](https://docs.litellm.ai/docs/providers/volcano) | ✅ | ✅ | ✅ | | | | | | | | | [Voyage AI (`voyage`)](https://docs.litellm.ai/docs/providers/voyage) | | | | ✅ | | | | | | | diff --git a/litellm/constants.py b/litellm/constants.py index 69ff3cf7a5e..739ed873e07 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -966,6 +966,7 @@ openai_compatible_endpoints: Final[list] = [ "https://api.cortecs.ai/v1", "https://api.scx.ai/v1", "https://api.prisminference.com/v1", + "https://api.viktor.com/api/compat/v1", "https://gigachat.devices.sberbank.ru/api/v1", ] diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index 61ff4be3a46..bf39e88d709 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -218,5 +218,11 @@ "api_key_env": "SAIL_API_KEY", "api_base_env": "SAIL_API_BASE", "supported_endpoints": ["/v1/chat/completions", "/v1/responses", "/v1/messages"] + }, + "viktor": { + "base_url": "https://api.viktor.com/api/compat/v1", + "api_key_env": "VIKTOR_API_KEY", + "api_base_env": "VIKTOR_API_BASE", + "supported_endpoints": ["/v1/chat/completions", "/v1/responses"] } } diff --git a/litellm/provider_endpoints_support_backup.json b/litellm/provider_endpoints_support_backup.json index c9635587eeb..3d73f1249e0 100644 --- a/litellm/provider_endpoints_support_backup.json +++ b/litellm/provider_endpoints_support_backup.json @@ -2298,6 +2298,23 @@ "interactions": true } }, + "viktor": { + "display_name": "Viktor (`viktor`)", + "url": "https://docs.litellm.ai/docs/providers/viktor", + "endpoints": { + "chat_completions": true, + "messages": false, + "responses": true, + "embeddings": false, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false, + "a2a": false + } + }, "vllm": { "display_name": "VLLM (`vllm`)", "url": "https://docs.litellm.ai/docs/providers/vllm", diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index 6e96d6ad0ec..f50a18067b3 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -3515,6 +3515,34 @@ ], "default_model_placeholder": "voyage/" }, + { + "provider": "Viktor", + "provider_display_name": "Viktor", + "litellm_provider": "viktor", + "credential_fields": [ + { + "key": "api_base", + "label": "API Base", + "placeholder": "https://api.viktor.com/api/compat/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": "viktor/viktor" + }, { "provider": "WANDB", "provider_display_name": "Wandb", diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 142f9b14a72..575ea4e5a16 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -4177,6 +4177,7 @@ class LlmProviders(str, Enum): CORTECS = "cortecs" SCX_AI = "scx-ai" PRISM = "prism" + VIKTOR = "viktor" DARKBLOOM = "darkbloom" META = "meta" SAIL = "sail" diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 7ffaacdb3aa..ec7f962234b 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -2673,6 +2673,23 @@ "interactions": true } }, + "viktor": { + "display_name": "Viktor (`viktor`)", + "url": "https://docs.litellm.ai/docs/providers/viktor", + "endpoints": { + "chat_completions": true, + "messages": false, + "responses": true, + "embeddings": false, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false, + "a2a": false + } + }, "vllm": { "display_name": "VLLM (`vllm`)", "url": "https://docs.litellm.ai/docs/providers/vllm", diff --git a/tests/unit/llms/openai_like/test_viktor_provider.py b/tests/unit/llms/openai_like/test_viktor_provider.py new file mode 100644 index 00000000000..47f460c1c16 --- /dev/null +++ b/tests/unit/llms/openai_like/test_viktor_provider.py @@ -0,0 +1,148 @@ +import json +from pathlib import Path + +import httpx +import pytest +import respx + +import litellm +from litellm.llms.openai_like.json_loader import JSONProviderRegistry + +CHAT_COMPLETION = { + "id": "chatcmpl-1", + "object": "chat.completion", + "created": 1, + "model": "viktor", + "choices": [{"index": 0, "message": {"role": "assistant", "content": "hello"}, "finish_reason": "stop"}], + "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, +} + +RESPONSE = { + "id": "resp-1", + "object": "response", + "created_at": 1, + "status": "completed", + "model": "viktor", + "output": [ + { + "type": "message", + "id": "msg_1", + "role": "assistant", + "status": "completed", + "content": [{"type": "output_text", "text": "hello", "annotations": []}], + } + ], + "usage": {"input_tokens": 1, "output_tokens": 1, "total_tokens": 2}, +} + +MESSAGES = [{"role": "user", "content": "hi"}] + + +@pytest.fixture +def base_url() -> str: + provider = JSONProviderRegistry.get("viktor") + assert provider is not None + return provider.base_url + + +@pytest.fixture(autouse=True) +def viktor_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("VIKTOR_API_KEY", "env-key") + monkeypatch.delenv("VIKTOR_API_BASE", raising=False) + + +@pytest.fixture +def viktor_http(): + with respx.mock(assert_all_called=False) as router: + yield router + + +def test_completion_posts_to_viktor_with_the_env_key(viktor_http: respx.MockRouter, base_url: str) -> None: + route = viktor_http.post(f"{base_url}/chat/completions").mock( + return_value=httpx.Response(200, json=CHAT_COMPLETION) + ) + + response = litellm.completion(model="viktor/viktor", messages=MESSAGES, max_completion_tokens=256) + + assert response.choices[0].message.content == "hello" + request = route.calls.last.request + assert request.headers["authorization"] == "Bearer env-key" + body = json.loads(request.content) + assert body["model"] == "viktor" + assert body["max_completion_tokens"] == 256 + + +def test_responses_posts_to_viktor(viktor_http: respx.MockRouter, base_url: str) -> None: + route = viktor_http.post(f"{base_url}/responses").mock(return_value=httpx.Response(200, json=RESPONSE)) + + response = litellm.responses(model="viktor/viktor", input="hi") + + assert response.output[-1].content[0].text == "hello" + assert json.loads(route.calls.last.request.content)["model"] == "viktor" + + +def test_api_base_env_override_is_used(viktor_http: respx.MockRouter, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("VIKTOR_API_BASE", "https://gateway.example/api/compat/v1") + route = viktor_http.post("https://gateway.example/api/compat/v1/chat/completions").mock( + return_value=httpx.Response(200, json=CHAT_COMPLETION) + ) + + litellm.completion(model="viktor/viktor", messages=MESSAGES) + + assert route.called + + +def test_explicit_api_base_and_key_win(viktor_http: respx.MockRouter) -> None: + route = viktor_http.post("https://explicit.example/v1/chat/completions").mock( + return_value=httpx.Response(200, json=CHAT_COMPLETION) + ) + + litellm.completion( + model="viktor/viktor", messages=MESSAGES, api_base="https://explicit.example/v1", api_key="explicit-key" + ) + + assert route.calls.last.request.headers["authorization"] == "Bearer explicit-key" + + +def test_unprefixed_model_on_the_viktor_base_url_uses_the_viktor_key( + viktor_http: respx.MockRouter, base_url: str +) -> None: + route = viktor_http.post(f"{base_url}/chat/completions").mock( + return_value=httpx.Response(200, json=CHAT_COMPLETION) + ) + + litellm.completion(model="agent", messages=MESSAGES, api_base=base_url) + + assert route.calls.last.request.headers["authorization"] == "Bearer env-key" + + +def test_router_deployment_reaches_viktor(viktor_http: respx.MockRouter, base_url: str) -> None: + route = viktor_http.post(f"{base_url}/chat/completions").mock( + return_value=httpx.Response(200, json=CHAT_COMPLETION) + ) + router = litellm.Router(model_list=[{"model_name": "agent", "litellm_params": {"model": "viktor/viktor"}}]) + + response = router.completion(model="agent", messages=MESSAGES) + + assert response.choices[0].message.content == "hello" + assert route.called + + +def test_transcription_is_refused_without_calling_viktor(viktor_http: respx.MockRouter) -> None: + catch_all = viktor_http.route().mock(return_value=httpx.Response(500)) + + with pytest.raises(ValueError, match="Unmapped provider"): + litellm.transcription(model="viktor/viktor", file=("a.wav", b"RIFF", "audio/wav")) + + assert not catch_all.called + + +def test_endpoint_matrix_matches_its_backup() -> None: + def endpoints(path: Path) -> dict: + return json.loads(path.read_text())["providers"]["viktor"]["endpoints"] + + root = Path(__file__).parents[4] / "provider_endpoints_support.json" + backup = Path(litellm.__file__).parent / "provider_endpoints_support_backup.json" + + assert endpoints(root) == endpoints(backup) + assert endpoints(root)["responses"] is JSONProviderRegistry.supports_responses_api("viktor")