Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions litellm/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]

Expand Down Expand Up @@ -1038,6 +1039,7 @@
"cognition",
"cortecs",
"scx-ai",
"sokkan",
"prism",
"sail",
]
Expand Down
6 changes: 6 additions & 0 deletions litellm/llms/openai_like/providers.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
40 changes: 40 additions & 0 deletions litellm/model_prices_and_context_window_backup.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
17 changes: 17 additions & 0 deletions litellm/provider_endpoints_support_backup.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
28 changes: 28 additions & 0 deletions litellm/proxy/public_endpoints/provider_create_fields.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions litellm/types/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4155,6 +4155,7 @@ class LlmProviders(str, Enum):
CORTECS = "cortecs"
SCX_AI = "scx-ai"
PRISM = "prism"
SOKKAN = "sokkan"
DARKBLOOM = "darkbloom"
META = "meta"
SAIL = "sail"
Expand Down
40 changes: 40 additions & 0 deletions model_prices_and_context_window.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
17 changes: 17 additions & 0 deletions provider_endpoints_support.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
138 changes: 138 additions & 0 deletions tests/unit/llms/openai_like/test_sokkan_provider.py
Original file line number Diff line number Diff line change
@@ -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"
Comment on lines +35 to +45

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Chat request remains untested

These tests verify credential and model resolution but never send a chat-completions request. A regression in the request URL, bearer header, or slash-containing model ID could therefore go unnoticed. Add a mocked request test that checks what is actually sent.

Knowledge Base Used: Provider adapters and capabilities

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in fbd2e7d: test_sokkan_chat_completion_request mocks the upstream with respx and checks the URL, the bearer header, and the slash-containing model ID in the request body.



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"
Loading