feat(provider): add sference as an OpenAI-compatible provider

Registers sference (https://api.sference.com/v1) through the JSON-based openai_like provider config and adds the six models from its /v1/models catalog to the cost map with pricing, context ceilings, and capabilities

Adds a default_capabilities flag to the JSON provider config so uncataloged sference models (BYOM) keep tool calling, and declares the native Anthropic /v1/messages endpoint so the generic JSON messages passthrough activates

Also wires sference into models_by_provider for wildcard proxy deployments, the validate_environment key check, api_base autodetection with explicit-key precedence, and the runtime-served supported-endpoints matrix (litellm/provider_endpoints_support_backup.json)

Adds unit tests for provider resolution, param mapping, catalog integrity, messages passthrough, cost calculation, and a mocked completion round trip
This commit is contained in:
jernejstrasner 2026-08-09 16:22:34 +02:00
parent ecba48dd7c
commit fc968c42f3
No known key found for this signature in database
14 changed files with 946 additions and 1 deletions

View file

@ -644,6 +644,7 @@ dashscope_models: Set = set()
moonshot_models: Set = set()
publicai_models: Set = set()
darkbloom_models: Set = set()
sference_models: Set = set()
v0_models: Set = set()
morph_models: Set = set()
lambda_ai_models: Set = set()
@ -897,6 +898,8 @@ def _populate_provider_model_sets(model_cost_map: Dict) -> None:
publicai_models.add(key)
elif value.get("litellm_provider") == "darkbloom":
darkbloom_models.add(key)
elif value.get("litellm_provider") == "sference":
sference_models.add(key)
elif value.get("litellm_provider") == "v0":
v0_models.add(key)
elif value.get("litellm_provider") == "morph":
@ -1055,6 +1058,7 @@ model_list = list(
| moonshot_models
| publicai_models
| darkbloom_models
| sference_models
| v0_models
| morph_models
| lambda_ai_models
@ -1162,6 +1166,7 @@ def _build_models_by_provider() -> dict:
"moonshot": moonshot_models,
"publicai": publicai_models,
"darkbloom": darkbloom_models,
"sference": sference_models,
"v0": v0_models,
"morph": morph_models,
"lambda_ai": lambda_ai_models,

View file

@ -752,6 +752,7 @@ openai_compatible_endpoints: Final[list] = [
"https://api.libertai.io/v1",
"https://pinstripes.io/v1",
"https://api.meta.ai/v1",
"https://api.sference.com/v1",
]
@ -819,6 +820,7 @@ openai_compatible_providers: Final[list] = [
"pinstripes", # Pinstripes - JSON-configured provider
"darkbloom",
"meta", # Meta Model API (Muse Spark) - JSON-configured provider
"sference", # sference - JSON-configured provider
]
openai_text_completion_compatible_providers: Final[list] = [ # providers that support `/v1/completions`
"together_ai",

View file

@ -349,6 +349,9 @@ def get_llm_provider(
elif endpoint == "https://api.meta.ai/v1":
custom_llm_provider = "meta"
dynamic_api_key = get_secret_str("META_API_KEY")
elif endpoint == "https://api.sference.com/v1":
custom_llm_provider = "sference"
dynamic_api_key = api_key or get_secret_str("SFERENCE_API_KEY")
if api_base is not None and not isinstance(api_base, str):
raise Exception(f"api base needs to be a string. api_base={api_base}")

View file

@ -89,6 +89,11 @@ def create_config_class(provider: SimpleProviderConfig):
return api_base
def _model_in_cost_map(self, model: str) -> bool:
import litellm
return model in litellm.model_cost or f"{provider.slug}/{model}" in litellm.model_cost
def get_supported_openai_params(self, model: str) -> list:
"""Get supported OpenAI params, excluding tool-related params for models
that don't support function calling."""
@ -96,7 +101,9 @@ def create_config_class(provider: SimpleProviderConfig):
supported_params: Final = super().get_supported_openai_params(model=model)
_supports_fc: Final = supports_function_calling(model=model, custom_llm_provider=provider.slug)
_supports_fc: Final = supports_function_calling(model=model, custom_llm_provider=provider.slug) or (
provider.default_capabilities.get("function_calling") is True and not self._model_in_cost_map(model)
)
if not _supports_fc:
tool_params: Final = [

View file

@ -22,6 +22,7 @@ class SimpleProviderConfig:
self.constraints = data.get("constraints", {})
self.special_handling = data.get("special_handling", {})
self.supported_endpoints = data.get("supported_endpoints", [])
self.default_capabilities = data.get("default_capabilities", {})
class JSONProviderRegistry:

View file

@ -37,6 +37,16 @@
"max_completion_tokens": "max_tokens"
}
},
"sference": {
"base_url": "https://api.sference.com/v1",
"api_key_env": "SFERENCE_API_KEY",
"api_base_env": "SFERENCE_API_BASE",
"base_class": "openai_gpt",
"supported_endpoints": ["/v1/chat/completions", "/v1/messages"],
"default_capabilities": {
"function_calling": true
}
},
"apertis": {
"base_url": "https://api.stima.tech/v1",
"api_key_env": "STIMA_API_KEY",

View file

@ -33733,6 +33733,181 @@
"supports_vision": true,
"source": "https://cloud.sambanova.ai/plans/pricing"
},
"sference/Qwen/Qwen3.6-35B-A3B": {
"cache_read_input_token_cost": 5e-08,
"input_cost_per_token": 2e-07,
"litellm_provider": "sference",
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"max_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 1.25e-06,
"source": "https://api.sference.com/v1/models",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/messages"
],
"supported_modalities": [
"text"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
},
"sference/Qwen/Qwen3-VL-30B-A3B-Instruct": {
"cache_read_input_token_cost": 1e-07,
"input_cost_per_token": 4e-07,
"litellm_provider": "sference",
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"max_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 2e-06,
"source": "https://api.sference.com/v1/models",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/messages"
],
"supported_modalities": [
"text",
"image"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_prompt_caching": true,
"supports_reasoning": false,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true
},
"sference/bottlecapai/ThinkingCap-Qwen3.6-27B": {
"cache_read_input_token_cost": 5e-08,
"input_cost_per_token": 4e-07,
"litellm_provider": "sference",
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"max_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 2.6e-06,
"source": "https://api.sference.com/v1/models",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/messages"
],
"supported_modalities": [
"text"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
},
"sference/deepseek-ai/DeepSeek-V4-Flash": {
"cache_read_input_token_cost": 7e-08,
"input_cost_per_token": 1.4e-07,
"litellm_provider": "sference",
"max_input_tokens": 1048576,
"max_output_tokens": 1048576,
"max_tokens": 1048576,
"mode": "chat",
"output_cost_per_token": 2.8e-07,
"source": "https://api.sference.com/v1/models",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/messages"
],
"supported_modalities": [
"text"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
},
"sference/moonshotai/Kimi-K3": {
"cache_read_input_token_cost": 2.25e-07,
"input_cost_per_token": 2.25e-06,
"litellm_provider": "sference",
"max_input_tokens": 1048576,
"max_output_tokens": 1048576,
"max_tokens": 1048576,
"mode": "chat",
"output_cost_per_token": 1.125e-05,
"source": "https://api.sference.com/v1/models",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/messages"
],
"supported_modalities": [
"text"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
},
"sference/zai-org/GLM-5.2": {
"cache_read_input_token_cost": 2.6e-07,
"input_cost_per_token": 1.2e-06,
"litellm_provider": "sference",
"max_input_tokens": 1048576,
"max_output_tokens": 1048576,
"max_tokens": 1048576,
"mode": "chat",
"output_cost_per_token": 4.2e-06,
"source": "https://api.sference.com/v1/models",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/messages"
],
"supported_modalities": [
"text"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
},
"snowflake/claude-3-5-sonnet": {
"litellm_provider": "snowflake",
"max_input_tokens": 200000,

View file

@ -2010,6 +2010,23 @@
"interactions": true
}
},
"sference": {
"display_name": "Sference (`sference`)",
"url": "https://docs.litellm.ai/docs/providers/sference",
"endpoints": {
"chat_completions": true,
"messages": true,
"responses": false,
"embeddings": false,
"image_generations": false,
"audio_transcriptions": false,
"audio_speech": false,
"moderations": false,
"batches": false,
"rerank": false,
"a2a": false
}
},
"snowflake": {
"display_name": "Snowflake (`snowflake`)",
"url": "https://docs.litellm.ai/docs/providers/snowflake",

View file

@ -3706,6 +3706,7 @@ class LlmProviders(str, Enum):
PINSTRIPES = "pinstripes"
DARKBLOOM = "darkbloom"
META = "meta"
SFERENCE = "sference"
LITELLM_AGENT = "litellm_agent"
CURSOR = "cursor"
BEDROCK_MANTLE = "bedrock_mantle"

View file

@ -6151,6 +6151,11 @@ def validate_environment(
keys_in_environment = True
else:
missing_keys.append("TENCENT_API_KEY")
elif custom_llm_provider == "sference":
if "SFERENCE_API_KEY" in os.environ:
keys_in_environment = True
else:
missing_keys.append("SFERENCE_API_KEY")
elif custom_llm_provider == "mistral":
if "MISTRAL_API_KEY" in os.environ:
keys_in_environment = True

View file

@ -33824,6 +33824,181 @@
"supports_vision": true,
"source": "https://cloud.sambanova.ai/plans/pricing"
},
"sference/Qwen/Qwen3.6-35B-A3B": {
"cache_read_input_token_cost": 5e-08,
"input_cost_per_token": 2e-07,
"litellm_provider": "sference",
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"max_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 1.25e-06,
"source": "https://api.sference.com/v1/models",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/messages"
],
"supported_modalities": [
"text"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
},
"sference/Qwen/Qwen3-VL-30B-A3B-Instruct": {
"cache_read_input_token_cost": 1e-07,
"input_cost_per_token": 4e-07,
"litellm_provider": "sference",
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"max_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 2e-06,
"source": "https://api.sference.com/v1/models",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/messages"
],
"supported_modalities": [
"text",
"image"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_prompt_caching": true,
"supports_reasoning": false,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true
},
"sference/bottlecapai/ThinkingCap-Qwen3.6-27B": {
"cache_read_input_token_cost": 5e-08,
"input_cost_per_token": 4e-07,
"litellm_provider": "sference",
"max_input_tokens": 262144,
"max_output_tokens": 262144,
"max_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 2.6e-06,
"source": "https://api.sference.com/v1/models",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/messages"
],
"supported_modalities": [
"text"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
},
"sference/deepseek-ai/DeepSeek-V4-Flash": {
"cache_read_input_token_cost": 7e-08,
"input_cost_per_token": 1.4e-07,
"litellm_provider": "sference",
"max_input_tokens": 1048576,
"max_output_tokens": 1048576,
"max_tokens": 1048576,
"mode": "chat",
"output_cost_per_token": 2.8e-07,
"source": "https://api.sference.com/v1/models",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/messages"
],
"supported_modalities": [
"text"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
},
"sference/moonshotai/Kimi-K3": {
"cache_read_input_token_cost": 2.25e-07,
"input_cost_per_token": 2.25e-06,
"litellm_provider": "sference",
"max_input_tokens": 1048576,
"max_output_tokens": 1048576,
"max_tokens": 1048576,
"mode": "chat",
"output_cost_per_token": 1.125e-05,
"source": "https://api.sference.com/v1/models",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/messages"
],
"supported_modalities": [
"text"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
},
"sference/zai-org/GLM-5.2": {
"cache_read_input_token_cost": 2.6e-07,
"input_cost_per_token": 1.2e-06,
"litellm_provider": "sference",
"max_input_tokens": 1048576,
"max_output_tokens": 1048576,
"max_tokens": 1048576,
"mode": "chat",
"output_cost_per_token": 4.2e-06,
"source": "https://api.sference.com/v1/models",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/messages"
],
"supported_modalities": [
"text"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
},
"snowflake/claude-3-5-sonnet": {
"litellm_provider": "snowflake",
"max_input_tokens": 200000,

View file

@ -2244,6 +2244,23 @@
"interactions": true
}
},
"sference": {
"display_name": "Sference (`sference`)",
"url": "https://docs.litellm.ai/docs/providers/sference",
"endpoints": {
"chat_completions": true,
"messages": true,
"responses": false,
"embeddings": false,
"image_generations": false,
"audio_transcriptions": false,
"audio_speech": false,
"moderations": false,
"batches": false,
"rerank": false,
"a2a": false
}
},
"snowflake": {
"display_name": "Snowflake (`snowflake`)",
"url": "https://docs.litellm.ai/docs/providers/snowflake",

View file

@ -0,0 +1,393 @@
"""
Tests for the sference provider configuration and integration.
"""
import json
import os
from pathlib import Path
import pytest
import litellm
@pytest.fixture(scope="module", autouse=True)
def local_model_cost_map():
"""Force the bundled cost map so catalog-dependent assertions see this branch's
sference entries, then restore the prior global state."""
original_model_cost = litellm.model_cost
previous_env = os.environ.get("LITELLM_LOCAL_MODEL_COST_MAP")
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
litellm.model_cost = litellm.get_model_cost_map(url="")
try:
yield
finally:
litellm.model_cost = original_model_cost
if previous_env is None:
os.environ.pop("LITELLM_LOCAL_MODEL_COST_MAP", None)
else:
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = previous_env
@pytest.fixture(autouse=True)
def clear_sference_env(monkeypatch):
"""Keep tests hermetic when the host exports sference env vars."""
monkeypatch.delenv("SFERENCE_API_BASE", raising=False)
monkeypatch.delenv("SFERENCE_API_KEY", raising=False)
class TestSferenceProviderConfig:
def test_sference_in_provider_list(self):
from litellm import LlmProviders
assert hasattr(LlmProviders, "SFERENCE")
assert LlmProviders.SFERENCE.value == "sference"
assert "sference" in litellm.provider_list
def test_sference_json_config_exists(self):
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
assert JSONProviderRegistry.exists("sference")
sference = JSONProviderRegistry.get("sference")
assert sference is not None
assert sference.base_url == "https://api.sference.com/v1"
assert sference.api_key_env == "SFERENCE_API_KEY"
assert sference.api_base_env == "SFERENCE_API_BASE"
def test_sference_in_openai_compatible_providers(self):
from litellm.constants import openai_compatible_providers
assert "sference" in openai_compatible_providers
def test_sference_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="sference/Qwen/Qwen3.6-35B-A3B",
custom_llm_provider=None,
api_base=None,
api_key="sk-test",
)
assert model == "Qwen/Qwen3.6-35B-A3B"
assert provider == "sference"
assert api_base == "https://api.sference.com/v1"
assert api_key == "sk-test"
def test_sference_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="sference/Qwen/Qwen3.6-35B-A3B",
custom_llm_provider=None,
api_base="https://custom.sference.example/v1",
api_key="sk-test",
)
assert provider == "sference"
assert api_base == "https://custom.sference.example/v1"
assert api_key == "sk-test"
def test_sference_url_autodetection(self):
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
model, provider, api_key, api_base = get_llm_provider(
model="Qwen/Qwen3.6-35B-A3B",
custom_llm_provider=None,
api_base="https://api.sference.com/v1",
api_key=None,
)
assert provider == "sference"
assert api_base == "https://api.sference.com/v1"
def test_explicit_api_key_wins_over_env(self, monkeypatch):
"""In the api_base autodetection branch an explicit per-request key must
beat the host-exported SFERENCE_API_KEY, otherwise requests bill the wrong account."""
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
monkeypatch.setenv("SFERENCE_API_KEY", "sk-env-key")
model, provider, api_key, api_base = get_llm_provider(
model="Qwen/Qwen3.6-35B-A3B",
custom_llm_provider=None,
api_base="https://api.sference.com/v1",
api_key="sk-explicit",
)
assert provider == "sference"
assert api_key == "sk-explicit"
def test_sference_api_key_resolved_from_env(self, monkeypatch):
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
monkeypatch.setenv("SFERENCE_API_KEY", "sk-env-key")
model, provider, api_key, api_base = get_llm_provider(
model="sference/Qwen/Qwen3.6-35B-A3B",
custom_llm_provider=None,
api_base=None,
api_key=None,
)
assert provider == "sference"
assert api_key == "sk-env-key"
def test_sference_complete_url(self):
from litellm.llms.openai_like.dynamic_config import create_config_class
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
provider = JSONProviderRegistry.get("sference")
assert provider is not None
cfg = create_config_class(provider)()
url = cfg.get_complete_url(
api_base=None,
api_key="sk-test",
model="Qwen/Qwen3.6-35B-A3B",
optional_params={},
litellm_params={},
)
assert url == "https://api.sference.com/v1/chat/completions"
class TestSferenceSupportedParams:
def test_reasoning_model_supports_reasoning_effort(self):
params = litellm.get_supported_openai_params(model="Qwen/Qwen3.6-35B-A3B", custom_llm_provider="sference")
assert params is not None
assert "reasoning_effort" in params
def test_non_reasoning_model_excludes_reasoning_effort(self):
"""Qwen3-VL is listed with thinking unsupported in the sference catalog,
so it must not advertise reasoning_effort."""
params = litellm.get_supported_openai_params(
model="Qwen/Qwen3-VL-30B-A3B-Instruct", custom_llm_provider="sference"
)
assert params is not None
assert "reasoning_effort" not in params
def test_tool_params_supported(self):
params = litellm.get_supported_openai_params(model="Qwen/Qwen3.6-35B-A3B", custom_llm_provider="sference")
assert params is not None
assert "tools" in params
assert "tool_choice" in params
def test_service_tier_and_prompt_cache_key_supported(self):
params = litellm.get_supported_openai_params(model="Qwen/Qwen3.6-35B-A3B", custom_llm_provider="sference")
assert params is not None
assert "service_tier" in params
assert "prompt_cache_key" in params
def test_reasoning_effort_and_service_tier_mapped_through(self):
cfg = litellm.ProviderConfigManager.get_provider_chat_config(
model="Qwen/Qwen3.6-35B-A3B", provider=litellm.LlmProviders.SFERENCE
)
assert cfg is not None
mapped = cfg.map_openai_params(
non_default_params={"reasoning_effort": "high", "service_tier": "flex"},
optional_params={},
model="Qwen/Qwen3.6-35B-A3B",
drop_params=False,
)
assert mapped["reasoning_effort"] == "high"
assert mapped["service_tier"] == "flex"
class TestSferenceCompletion:
@pytest.mark.respx()
def test_completion_request_routing(self, respx_mock, monkeypatch):
"""A completion call must hit the sference chat completions endpoint with the
catalog model id (provider prefix stripped), bearer auth, and pass-through params."""
monkeypatch.setenv("SFERENCE_API_KEY", "sk-env-key")
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
respx_mock.post("https://api.sference.com/v1/chat/completions").respond(
json={
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1786206392,
"model": "Qwen/Qwen3.6-35B-A3B",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Hello!"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 5, "completion_tokens": 3, "total_tokens": 8},
},
status_code=200,
)
response = litellm.completion(
model="sference/Qwen/Qwen3.6-35B-A3B",
messages=[{"role": "user", "content": "hi"}],
reasoning_effort="high",
service_tier="flex",
)
assert len(respx_mock.calls) == 1
request = respx_mock.calls[0].request
assert request.headers["Authorization"] == "Bearer sk-env-key"
body = json.loads(request.content)
assert body["model"] == "Qwen/Qwen3.6-35B-A3B"
assert body["reasoning_effort"] == "high"
assert body["service_tier"] == "flex"
assert response.choices[0].message.content == "Hello!"
class TestSferenceValidateEnvironment:
def test_missing_api_key_reported(self):
result = litellm.validate_environment(model="sference/Qwen/Qwen3.6-35B-A3B")
assert result["keys_in_environment"] is False
assert "SFERENCE_API_KEY" in result["missing_keys"]
def test_present_api_key_accepted(self, monkeypatch):
monkeypatch.setenv("SFERENCE_API_KEY", "sk-env-key")
result = litellm.validate_environment(model="sference/Qwen/Qwen3.6-35B-A3B")
assert result["keys_in_environment"] is True
assert result["missing_keys"] == []
class TestSferenceModelsByProvider:
def test_models_by_provider_contains_catalog(self):
"""Wildcard sference/* proxy deployments expand via models_by_provider."""
litellm.add_known_models(litellm.model_cost)
sference_models = litellm.models_by_provider.get("sference")
assert sference_models is not None
assert len(sference_models) == 6
assert "sference/moonshotai/Kimi-K3" in sference_models
class TestSferenceRuntimeEndpointsMatrix:
def test_sference_in_runtime_supported_endpoints_matrix(self):
"""The proxy serves litellm/provider_endpoints_support_backup.json at
GET /public/supported_endpoints, so sference must be listed there too."""
matrix = json.loads((Path(litellm.__file__).parent / "provider_endpoints_support_backup.json").read_text())
assert "sference" in matrix["providers"]
endpoints = matrix["providers"]["sference"]["endpoints"]
assert endpoints["chat_completions"] is True
assert endpoints["messages"] is True
class TestSferenceUncatalogedModels:
def test_uncataloged_model_keeps_tool_params(self):
"""BYOM/custom sference models absent from the cost map fall back to the
provider-level default capability instead of losing tool calling."""
params = litellm.get_supported_openai_params(model="my-org/my-fine-tune", custom_llm_provider="sference")
assert params is not None
assert "tools" in params
assert "tool_choice" in params
def test_uncataloged_model_has_no_reasoning_effort(self):
params = litellm.get_supported_openai_params(model="my-org/my-fine-tune", custom_llm_provider="sference")
assert params is not None
assert "reasoning_effort" not in params
class TestSferenceAnthropicMessages:
def test_messages_config_resolves(self):
from litellm.llms.openai_like.messages.transformation import (
JSONProviderAnthropicMessagesConfig,
)
cfg = litellm.ProviderConfigManager.get_provider_anthropic_messages_config(
model="Qwen/Qwen3.6-35B-A3B", provider=litellm.LlmProviders.SFERENCE
)
assert isinstance(cfg, JSONProviderAnthropicMessagesConfig)
def test_messages_complete_url(self):
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
from litellm.llms.openai_like.messages.transformation import (
JSONProviderAnthropicMessagesConfig,
)
provider = JSONProviderRegistry.get("sference")
assert provider is not None
cfg = JSONProviderAnthropicMessagesConfig(provider)
url = cfg.get_complete_url(
api_base=None,
api_key="sk-test",
model="Qwen/Qwen3.6-35B-A3B",
optional_params={},
litellm_params={},
)
assert url == "https://api.sference.com/v1/messages"
def test_messages_api_key_resolved_from_env(self, monkeypatch):
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
from litellm.llms.openai_like.messages.transformation import (
JSONProviderAnthropicMessagesConfig,
)
monkeypatch.setenv("SFERENCE_API_KEY", "sk-env-key")
provider = JSONProviderRegistry.get("sference")
assert provider is not None
cfg = JSONProviderAnthropicMessagesConfig(provider)
headers, _ = cfg.validate_anthropic_messages_environment(
headers={},
model="Qwen/Qwen3.6-35B-A3B",
messages=[{"role": "user", "content": "hi"}],
optional_params={},
litellm_params={},
api_key=None,
api_base=None,
)
assert headers["authorization"] == "Bearer sk-env-key"
assert headers["anthropic-version"] == "2023-06-01"
class TestSferenceModelInfoAndCost:
def test_model_info_from_catalog(self):
"""get_model_info resolves the catalog entry; the values themselves are
pinned in test_sference_model_metadata.py."""
info = litellm.get_model_info("sference/deepseek-ai/DeepSeek-V4-Flash")
assert info["litellm_provider"] == "sference"
assert info["max_input_tokens"] == info["max_output_tokens"]
assert info["supports_reasoning"] is True
assert info["supports_function_calling"] is True
assert info["supports_prompt_caching"] is True
assert info["supports_vision"] is False
def test_get_max_tokens_from_catalog(self):
"""get_max_tokens must not raise for cataloged sference models; the pinned
value is the published context ceiling."""
assert litellm.get_max_tokens("sference/moonshotai/Kimi-K3") == 1048576
assert litellm.get_max_tokens("sference/Qwen/Qwen3.6-35B-A3B") == 262144
def test_vision_model_info_from_catalog(self):
info = litellm.get_model_info("sference/Qwen/Qwen3-VL-30B-A3B-Instruct")
assert info["litellm_provider"] == "sference"
assert info["supports_vision"] is True
assert info["supports_reasoning"] is False
def test_cost_calculation_with_cached_tokens(self):
"""Cost math must combine uncached input at input price, cached input at
cache-read price, and output at output price, all from the catalog entry."""
from litellm import completion_cost
from litellm.types.utils import ModelResponse, PromptTokensDetails, Usage
entry = litellm.model_cost["sference/deepseek-ai/DeepSeek-V4-Flash"]
response = ModelResponse(
model="deepseek-ai/DeepSeek-V4-Flash",
usage=Usage(
prompt_tokens=1000,
completion_tokens=500,
total_tokens=1500,
prompt_tokens_details=PromptTokensDetails(cached_tokens=200),
),
)
cost = completion_cost(
completion_response=response,
model="sference/deepseek-ai/DeepSeek-V4-Flash",
custom_llm_provider="sference",
)
expected = (
800 * entry["input_cost_per_token"]
+ 200 * entry["cache_read_input_token_cost"]
+ 500 * entry["output_cost_per_token"]
)
assert abs(cost - expected) < 1e-12

View file

@ -0,0 +1,134 @@
"""
Validates the sference catalog entries in model_prices_and_context_window.json
against the sference /v1/models source data, and that the bundled backup stays
in sync.
"""
import json
from pathlib import Path
import pytest
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
@pytest.fixture(autouse=True)
def clear_sference_env(monkeypatch):
"""Keep routing assertions hermetic when the host exports sference env vars."""
monkeypatch.delenv("SFERENCE_API_BASE", raising=False)
monkeypatch.delenv("SFERENCE_API_KEY", raising=False)
SFERENCE_SOURCE = "https://api.sference.com/v1/models"
SFERENCE_MODELS = {
"sference/Qwen/Qwen3.6-35B-A3B": {
"input_cost_per_token": 2e-07,
"output_cost_per_token": 1.25e-06,
"cache_read_input_token_cost": 5e-08,
"max_input_tokens": 262144,
"supports_reasoning": True,
"supports_vision": False,
"supported_modalities": ["text"],
},
"sference/Qwen/Qwen3-VL-30B-A3B-Instruct": {
"input_cost_per_token": 4e-07,
"output_cost_per_token": 2e-06,
"cache_read_input_token_cost": 1e-07,
"max_input_tokens": 262144,
"supports_reasoning": False,
"supports_vision": True,
"supported_modalities": ["text", "image"],
},
"sference/bottlecapai/ThinkingCap-Qwen3.6-27B": {
"input_cost_per_token": 4e-07,
"output_cost_per_token": 2.6e-06,
"cache_read_input_token_cost": 5e-08,
"max_input_tokens": 262144,
"supports_reasoning": True,
"supports_vision": False,
"supported_modalities": ["text"],
},
"sference/deepseek-ai/DeepSeek-V4-Flash": {
"input_cost_per_token": 1.4e-07,
"output_cost_per_token": 2.8e-07,
"cache_read_input_token_cost": 7e-08,
"max_input_tokens": 1048576,
"supports_reasoning": True,
"supports_vision": False,
"supported_modalities": ["text"],
},
"sference/moonshotai/Kimi-K3": {
"input_cost_per_token": 2.25e-06,
"output_cost_per_token": 1.125e-05,
"cache_read_input_token_cost": 2.25e-07,
"max_input_tokens": 1048576,
"supports_reasoning": True,
"supports_vision": False,
"supported_modalities": ["text"],
},
"sference/zai-org/GLM-5.2": {
"input_cost_per_token": 1.2e-06,
"output_cost_per_token": 4.2e-06,
"cache_read_input_token_cost": 2.6e-07,
"max_input_tokens": 1048576,
"supports_reasoning": True,
"supports_vision": False,
"supported_modalities": ["text"],
},
}
def _load(path: Path) -> dict:
with open(path) as f:
return json.load(f)
@pytest.mark.parametrize("model", sorted(SFERENCE_MODELS))
def test_sference_catalog_entry(model: str):
model_cost = _load(Path(__file__).parents[2] / "model_prices_and_context_window.json")
info = model_cost.get(model)
assert info is not None, f"{model} not found in model_prices_and_context_window.json"
expected = SFERENCE_MODELS[model]
assert info["litellm_provider"] == "sference"
assert info["mode"] == "chat"
assert info["source"] == SFERENCE_SOURCE
assert info["input_cost_per_token"] == expected["input_cost_per_token"]
assert info["output_cost_per_token"] == expected["output_cost_per_token"]
assert info["cache_read_input_token_cost"] == expected["cache_read_input_token_cost"]
assert info["max_input_tokens"] == expected["max_input_tokens"]
assert info["max_output_tokens"] == expected["max_input_tokens"]
assert info["max_tokens"] == expected["max_input_tokens"]
assert info["supports_reasoning"] is expected["supports_reasoning"]
assert info["supports_vision"] is expected["supports_vision"]
assert info["supported_modalities"] == expected["supported_modalities"]
assert info["supported_output_modalities"] == ["text"]
assert info["supports_function_calling"] is True
assert info["supports_tool_choice"] is True
assert info["supports_prompt_caching"] is True
assert info["supports_response_schema"] is True
assert info["supports_system_messages"] is True
assert info["supports_native_streaming"] is True
assert info["supported_endpoints"] == ["/v1/chat/completions", "/v1/messages"]
@pytest.mark.parametrize("model", sorted(SFERENCE_MODELS))
def test_sference_backup_matches_main(model: str):
repo_root = Path(__file__).parents[2]
main_cost = _load(repo_root / "model_prices_and_context_window.json")
backup_cost = _load(repo_root / "litellm" / "model_prices_and_context_window_backup.json")
assert backup_cost.get(model) == main_cost.get(model), f"{model} differs between main and backup model cost maps"
@pytest.mark.parametrize("model", sorted(SFERENCE_MODELS))
def test_sference_model_routing(model: str):
routed_model, provider, _, api_base = get_llm_provider(model=model, api_key="sk-test")
assert routed_model == model.split("/", 1)[1]
assert provider == "sference"
assert api_base == "https://api.sference.com/v1"