fix(providers): keep Viktor out of the audio and image routing list

Viktor only serves chat completions and Responses, so it no longer joins openai_compatible_providers, which also routes transcription, speech and image calls. Base URL autodetection still works through the endpoint list

Tests now call completion, responses, transcription and the router against a mocked Viktor API and check the request, instead of checking registry flags
This commit is contained in:
Ivan Charapanau 2026-09-25 11:53:02 +00:00
parent 4f01ff13d5
commit 4d0467a795
2 changed files with 134 additions and 143 deletions

View file

@ -1021,7 +1021,6 @@ openai_compatible_providers: Final[list] = [
"meta", # Meta Model API (Muse Spark) - JSON-configured provider
"cognition",
"scx-ai",
"viktor", # Viktor - JSON-configured provider
]
OPENAI_AUDIO_TRANSCRIPTION_PROVIDERS: Final = frozenset({"openai"} | frozenset(openai_compatible_providers))

View file

@ -1,156 +1,148 @@
"""
Tests for Viktor provider configuration and integration.
"""
import json
from pathlib import Path
import httpx
import pytest
import respx
import litellm
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
VIKTOR_API_BASE = "https://api.viktor.com/api/compat/v1"
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"}]
class TestViktorProviderConfig:
def test_viktor_in_provider_list(self):
from litellm import LlmProviders
assert hasattr(LlmProviders, "VIKTOR")
assert LlmProviders.VIKTOR.value == "viktor"
assert "viktor" in litellm.provider_list
def test_viktor_json_config_exists(self):
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
assert JSONProviderRegistry.exists("viktor")
viktor = JSONProviderRegistry.get("viktor")
assert viktor is not None
assert viktor.base_url == VIKTOR_API_BASE
assert viktor.api_key_env == "VIKTOR_API_KEY"
assert viktor.api_base_env == "VIKTOR_API_BASE"
assert viktor.param_mappings == {}
assert viktor.constraints == {}
def test_viktor_supports_responses_api(self):
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
assert JSONProviderRegistry.supports_responses_api("viktor") is True
def test_viktor_in_openai_compatible_providers(self):
from litellm.constants import openai_compatible_endpoints, openai_compatible_providers
assert "viktor" in openai_compatible_providers
assert VIKTOR_API_BASE in openai_compatible_endpoints
def test_viktor_provider_resolution(self):
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
model, provider, api_key, api_base = get_llm_provider(
model="viktor/viktor",
custom_llm_provider=None,
api_base=None,
api_key=None,
)
assert model == "viktor"
assert provider == "viktor"
assert api_base == VIKTOR_API_BASE
def test_viktor_api_base_override(self):
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
model, provider, api_key, api_base = get_llm_provider(
model="viktor/viktor",
custom_llm_provider=None,
api_base="https://viktor-gateway.internal.example/api/compat/v1",
api_key="zt_test_sk_example",
)
assert provider == "viktor"
assert api_base == "https://viktor-gateway.internal.example/api/compat/v1"
assert api_key == "zt_test_sk_example"
def test_viktor_url_autodetection(self):
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
# Viktor serves one agent and does not validate the `model` field, so any
# un-prefixed model string sent to its base url should route to the provider
model, provider, api_key, api_base = get_llm_provider(
model="default",
custom_llm_provider=None,
api_base=VIKTOR_API_BASE,
api_key=None,
)
assert model == "default"
assert provider == "viktor"
assert api_base == VIKTOR_API_BASE
def test_viktor_env_api_key_is_read_from_viktor_variable(self, monkeypatch: pytest.MonkeyPatch):
from litellm.llms.openai_like.dynamic_config import create_config_class
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("VIKTOR_API_BASE", raising=False)
monkeypatch.setenv("VIKTOR_API_KEY", "zt_test_sk_env")
provider = JSONProviderRegistry.get("viktor")
assert provider is not None
api_base, api_key = create_config_class(provider)()._get_openai_compatible_provider_info(None, None)
assert api_base == VIKTOR_API_BASE
assert api_key == "zt_test_sk_env"
def test_viktor_max_completion_tokens_passed_through(self):
from litellm.llms.openai_like.dynamic_config import create_config_class
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
provider = JSONProviderRegistry.get("viktor")
assert provider is not None
config = create_config_class(provider)()
optional_params = config.map_openai_params(
non_default_params={"max_completion_tokens": 256},
optional_params={},
model="viktor",
drop_params=False,
)
assert optional_params["max_completion_tokens"] == 256
def test_viktor_router_config(self):
from litellm import Router
router = Router(
model_list=[
{
"model_name": "viktor",
"litellm_params": {
"model": "viktor/viktor",
"api_key": "test-key",
},
}
]
)
assert len(router.model_list) == 1
assert router.model_list[0]["model_name"] == "viktor"
@pytest.fixture
def base_url() -> str:
provider = JSONProviderRegistry.get("viktor")
assert provider is not None
return provider.base_url
class TestViktorEndpointMatrix:
@staticmethod
def _endpoints(path: Path) -> dict:
@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):
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"]
def test_viktor_supported_endpoints_matrix(self):
endpoints = self._endpoints(Path(litellm.__file__).parent / "provider_endpoints_support_backup.json")
root = Path(__file__).parents[4] / "provider_endpoints_support.json"
backup = Path(litellm.__file__).parent / "provider_endpoints_support_backup.json"
assert endpoints["chat_completions"] is True
assert endpoints["responses"] is True
assert endpoints["embeddings"] is False
def test_viktor_endpoint_matrix_synced_to_backup(self):
root = Path(__file__).parents[4] / "provider_endpoints_support.json"
backup = Path(litellm.__file__).parent / "provider_endpoints_support_backup.json"
assert self._endpoints(root) == self._endpoints(backup)
assert endpoints(root) == endpoints(backup)
assert endpoints(root)["responses"] is JSONProviderRegistry.supports_responses_api("viktor")