mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-07 07:58:09 +00:00
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:
parent
ecba48dd7c
commit
fc968c42f3
14 changed files with 946 additions and 1 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
393
tests/test_litellm/llms/openai_like/test_sference_provider.py
Normal file
393
tests/test_litellm/llms/openai_like/test_sference_provider.py
Normal 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
|
||||
134
tests/test_litellm/test_sference_model_metadata.py
Normal file
134
tests/test_litellm/test_sference_model_metadata.py
Normal 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"
|
||||
Loading…
Add table
Reference in a new issue