From fbd2e7d47d405a369bad18cf100a9a335b64c77e Mon Sep 17 00:00:00 2001 From: Nicolas Micaud <72915297+nicolas-micaud@users.noreply.github.com> Date: Fri, 2 Oct 2026 05:50:38 +0200 Subject: [PATCH] feat(providers): add SOKKAN as a JSON-configured OpenAI-compatible provider Registers `sokkan` (SOKKAN_API_KEY / SOKKAN_API_BASE, https://api.sokkan.ch/v1), Swiss-hosted zero-retention inference, with gpt-oss-20b and Qwen3-Coder-30B-A3B-Instruct in the cost map, the endpoint support matrix and the proxy add-model form. Tests check both models against independently stated rates and send a mocked chat-completions request. --- litellm/constants.py | 2 + litellm/llms/openai_like/providers.json | 6 + ...odel_prices_and_context_window_backup.json | 40 +++++ .../provider_endpoints_support_backup.json | 17 +++ .../provider_create_fields.json | 28 ++++ litellm/types/utils.py | 1 + model_prices_and_context_window.json | 40 +++++ provider_endpoints_support.json | 17 +++ .../llms/openai_like/test_sokkan_provider.py | 138 ++++++++++++++++++ 9 files changed, 289 insertions(+) create mode 100644 tests/unit/llms/openai_like/test_sokkan_provider.py diff --git a/litellm/constants.py b/litellm/constants.py index 76ab419272fc..1d679195339c 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -964,6 +964,7 @@ "https://api.cortecs.ai/v1", "https://api.scx.ai/v1", "https://api.prisminference.com/v1", + "https://api.sokkan.ch/v1", "https://gigachat.devices.sberbank.ru/api/v1", ] @@ -1038,6 +1039,7 @@ "cognition", "cortecs", "scx-ai", + "sokkan", "prism", "sail", ] diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index 61ff4be3a462..be14a864fe9b 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -213,6 +213,12 @@ "api_base_env": "PRISM_API_BASE", "supported_endpoints": ["/v1/chat/completions", "/v1/responses", "/v1/messages"] }, + "sokkan": { + "base_url": "https://api.sokkan.ch/v1", + "api_key_env": "SOKKAN_API_KEY", + "api_base_env": "SOKKAN_API_BASE", + "supported_endpoints": ["/v1/chat/completions"] + }, "sail": { "base_url": "https://api.sailresearch.com/v1", "api_key_env": "SAIL_API_KEY", diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 40fdf083cf89..9e870feabb16 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -79599,5 +79599,45 @@ "supports_response_schema": true, "supports_tool_choice": true, "supports_vision": true + }, + "sokkan/openai/gpt-oss-20b": { + "input_cost_per_token": 4e-08, + "litellm_provider": "sokkan", + "max_input_tokens": 65536, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1.5e-07, + "source": "https://api.sokkan.ch/v1/models", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "sokkan/qwen/qwen3-coder-30b-a3b-instruct": { + "input_cost_per_token": 7e-08, + "litellm_provider": "sokkan", + "max_input_tokens": 32768, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 2.7e-07, + "source": "https://api.sokkan.ch/v1/models", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_reasoning": false, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false } } diff --git a/litellm/provider_endpoints_support_backup.json b/litellm/provider_endpoints_support_backup.json index c9635587eeba..8adb11c10c46 100644 --- a/litellm/provider_endpoints_support_backup.json +++ b/litellm/provider_endpoints_support_backup.json @@ -1978,6 +1978,23 @@ "a2a": false } }, + "sokkan": { + "display_name": "SOKKAN (`sokkan`)", + "url": "https://sokkan.ch/en/api/", + "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, + "a2a": false + } + }, "recraft": { "display_name": "Recraft (`recraft`)", "url": "https://docs.litellm.ai/docs/providers/recraft", diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index 11d2ff61b952..4119ab0faa7a 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -2911,6 +2911,34 @@ ], "default_model_placeholder": "prism/deepseek-v4.1-flash" }, + { + "provider": "SOKKAN", + "provider_display_name": "SOKKAN", + "litellm_provider": "sokkan", + "credential_fields": [ + { + "key": "api_base", + "label": "API Base", + "placeholder": "https://api.sokkan.ch/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": "sokkan/qwen/qwen3-coder-30b-a3b-instruct" + }, { "provider": "RECRAFT", "provider_display_name": "Recraft", diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 8c10b9e3497f..71f20ce652d6 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -4155,6 +4155,7 @@ class LlmProviders(str, Enum): CORTECS = "cortecs" SCX_AI = "scx-ai" PRISM = "prism" + SOKKAN = "sokkan" DARKBLOOM = "darkbloom" META = "meta" SAIL = "sail" diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 40fdf083cf89..9e870feabb16 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -79599,5 +79599,45 @@ "supports_response_schema": true, "supports_tool_choice": true, "supports_vision": true + }, + "sokkan/openai/gpt-oss-20b": { + "input_cost_per_token": 4e-08, + "litellm_provider": "sokkan", + "max_input_tokens": 65536, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1.5e-07, + "source": "https://api.sokkan.ch/v1/models", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false + }, + "sokkan/qwen/qwen3-coder-30b-a3b-instruct": { + "input_cost_per_token": 7e-08, + "litellm_provider": "sokkan", + "max_input_tokens": 32768, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "output_cost_per_token": 2.7e-07, + "source": "https://api.sokkan.ch/v1/models", + "supported_endpoints": [ + "/v1/chat/completions" + ], + "supports_function_calling": true, + "supports_native_streaming": true, + "supports_reasoning": false, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_vision": false } } diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 9cbd326277e6..157301942b40 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -2230,6 +2230,23 @@ "a2a": false } }, + "sokkan": { + "display_name": "SOKKAN (`sokkan`)", + "url": "https://sokkan.ch/en/api/", + "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, + "a2a": false + } + }, "recraft": { "display_name": "Recraft (`recraft`)", "url": "https://docs.litellm.ai/docs/providers/recraft", diff --git a/tests/unit/llms/openai_like/test_sokkan_provider.py b/tests/unit/llms/openai_like/test_sokkan_provider.py new file mode 100644 index 000000000000..4c3c379700df --- /dev/null +++ b/tests/unit/llms/openai_like/test_sokkan_provider.py @@ -0,0 +1,138 @@ +import json +from pathlib import Path +from typing import Final + +import pytest +import respx + +import litellm +from litellm.caching.llm_caching_handler import LLMClientCache + +# Rates published by https://api.sokkan.ch/v1/models, stated independently of the cost map. +EXPECTED_SOKKAN_MODELS: Final = { + "sokkan/openai/gpt-oss-20b": { + "input_cost_per_token": 4e-08, + "output_cost_per_token": 1.5e-07, + "max_input_tokens": 65536, + "max_output_tokens": 32768, + "supports_reasoning": True, + }, + "sokkan/qwen/qwen3-coder-30b-a3b-instruct": { + "input_cost_per_token": 7e-08, + "output_cost_per_token": 2.7e-07, + "max_input_tokens": 32768, + "max_output_tokens": 16384, + "supports_reasoning": False, + }, +} + + +def test_sokkan_provider_resolution(monkeypatch: pytest.MonkeyPatch): + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + monkeypatch.setenv("SOKKAN_API_KEY", "sokkan-test-key") + + model, provider, api_key, api_base = get_llm_provider( + model="sokkan/qwen/qwen3-coder-30b-a3b-instruct", + custom_llm_provider=None, + api_base=None, + api_key=None, + ) + + assert model == "qwen/qwen3-coder-30b-a3b-instruct" + assert provider == "sokkan" + assert api_key == "sokkan-test-key" + assert api_base == "https://api.sokkan.ch/v1" + + +def test_sokkan_provider_keeps_explicit_credentials(monkeypatch: pytest.MonkeyPatch): + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + monkeypatch.setenv("SOKKAN_API_KEY", "sokkan-env-key") + + _, provider, api_key, api_base = get_llm_provider( + model="sokkan/openai/gpt-oss-20b", + custom_llm_provider=None, + api_base="https://sokkan.internal.example/v1", + api_key="sokkan-explicit-key", + ) + + assert provider == "sokkan" + assert api_key == "sokkan-explicit-key" + assert api_base == "https://sokkan.internal.example/v1" + + +def test_sokkan_cost_map_lists_exactly_the_expected_models(): + assert sorted(name for name in litellm.model_cost if name.startswith("sokkan/")) == sorted(EXPECTED_SOKKAN_MODELS) + + +@pytest.mark.parametrize("model,expected", sorted(EXPECTED_SOKKAN_MODELS.items())) +def test_sokkan_model_cost_and_capabilities(model: str, expected: dict): + from litellm.cost_calculator import cost_per_token + + prompt_cost, completion_cost = cost_per_token( + model=model, + prompt_tokens=1_000_000, + completion_tokens=1_000_000, + custom_llm_provider="sokkan", + ) + model_info = litellm.get_model_info(model) + + assert prompt_cost == pytest.approx(expected["input_cost_per_token"] * 1_000_000) + assert completion_cost == pytest.approx(expected["output_cost_per_token"] * 1_000_000) + assert model_info["max_input_tokens"] == expected["max_input_tokens"] + assert model_info["max_tokens"] == model_info["max_output_tokens"] == expected["max_output_tokens"] + assert model_info["supports_reasoning"] is expected["supports_reasoning"] + assert model_info["litellm_provider"] == "sokkan" + assert model_info["mode"] == "chat" + assert model_info["supports_function_calling"] is True + assert model_info["supports_native_streaming"] is True + assert model_info["supports_response_schema"] is True + assert litellm.supports_vision(model) is False + + +def test_sokkan_backup_registry_mirrors_cost_map(): + package_root = Path(litellm.__file__).parent + cost_map = json.loads((package_root.parent / "model_prices_and_context_window.json").read_text()) + backup = json.loads((package_root / "model_prices_and_context_window_backup.json").read_text()) + sokkan_entries = {name: entry for name, entry in cost_map.items() if name.startswith("sokkan/")} + + assert sorted(sokkan_entries) == sorted(EXPECTED_SOKKAN_MODELS) + assert sokkan_entries == {name: backup[name] for name in sokkan_entries} + + +def test_sokkan_chat_completion_request(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(litellm, "disable_aiohttp_transport", True) + monkeypatch.setattr(litellm, "in_memory_llm_clients_cache", LLMClientCache()) + with respx.mock() as upstream: + route: Final = upstream.post("https://api.sokkan.ch/v1/chat/completions").respond( + 200, + json={ + "id": "sk-test", + "object": "chat.completion", + "created": 1_790_000_000, + "model": "qwen/qwen3-coder-30b-a3b-instruct", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "Hello from SOKKAN"}, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 4, "completion_tokens": 3, "total_tokens": 7}, + }, + ) + response: Final = litellm.completion( + model="sokkan/qwen/qwen3-coder-30b-a3b-instruct", + messages=[{"role": "user", "content": "Say hello"}], + api_key="sokkan-test-key", + ) + + request: Final = route.calls.last.request + body: Final = json.loads(request.content) + assert route.call_count == 1 + assert str(request.url) == "https://api.sokkan.ch/v1/chat/completions" + assert request.headers["authorization"] == "Bearer sokkan-test-key" + assert body["model"] == "qwen/qwen3-coder-30b-a3b-instruct" + assert body["messages"] == [{"role": "user", "content": "Say hello"}] + assert response.choices[0].message.content == "Hello from SOKKAN"