feat(consus): add Consus Gateway as a native provider

Consus Gateway is OpenAI-compatible except it uses `x-api-key` instead
of `Authorization: Bearer`. Replaces the prior workaround of `openai/` +
`extra_headers` + a dummy api_key.

`ConsusChatConfig` extends `OpenAIGPTConfig` and overrides only
`validate_environment` and `_get_openai_compatible_provider_info`.
Registers 15 IL2 / IL5+ITAR models with zero-cost placeholders.

Tests: 11 unit + 3 mocked HTTP under `tests/test_litellm/llms/consus/`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ericmagliarditi 2026-05-07 15:47:54 -04:00
parent 93706bfb9a
commit cc0dfb01bd
No known key found for this signature in database
16 changed files with 833 additions and 0 deletions

View file

@ -237,6 +237,7 @@ azure_key: Optional[str] = None
anthropic_key: Optional[str] = None
replicate_key: Optional[str] = None
bytez_key: Optional[str] = None
consus_key: Optional[str] = None
cohere_key: Optional[str] = None
infinity_key: Optional[str] = None
clarifai_key: Optional[str] = None
@ -571,6 +572,7 @@ gemini_models: Set = set()
xai_models: Set = set()
zai_models: Set = set()
deepseek_models: Set = set()
consus_models: Set = set()
runwayml_models: Set = set()
azure_ai_models: Set = set()
jina_ai_models: Set = set()
@ -782,6 +784,8 @@ def add_known_models(model_cost_map: Optional[Dict] = None):
fal_ai_models.add(key)
elif value.get("litellm_provider") == "deepseek":
deepseek_models.add(key)
elif value.get("litellm_provider") == "consus":
consus_models.add(key)
elif value.get("litellm_provider") == "runwayml":
runwayml_models.add(key)
elif value.get("litellm_provider") == "meta_llama":
@ -964,6 +968,7 @@ model_list = list(
| zai_models
| fal_ai_models
| deepseek_models
| consus_models
| azure_ai_models
| voyage_models
| infinity_models
@ -1057,6 +1062,7 @@ models_by_provider: dict = {
"zai": zai_models,
"fal_ai": fal_ai_models,
"deepseek": deepseek_models,
"consus": consus_models,
"runwayml": runwayml_models,
"mistral": mistral_chat_models,
"azure_ai": azure_ai_models,
@ -1826,6 +1832,9 @@ if TYPE_CHECKING:
JinaAIEmbeddingConfig as JinaAIEmbeddingConfig,
)
from .llms.xai.chat.transformation import XAIChatConfig as XAIChatConfig
from .llms.consus.chat.transformation import (
ConsusChatConfig as ConsusChatConfig,
)
from .llms.zai.chat.transformation import ZAIChatConfig as ZAIChatConfig
from .llms.aiml.chat.transformation import AIMLChatConfig as AIMLChatConfig
from .llms.volcengine.chat.transformation import (

View file

@ -264,6 +264,7 @@ LLM_CONFIG_NAMES = (
"JinaAIEmbeddingConfig",
"XAIChatConfig",
"ZAIChatConfig",
"ConsusChatConfig",
"AIMLChatConfig",
"VolcEngineChatConfig",
"CodestralTextCompletionConfig",
@ -1031,6 +1032,7 @@ _LLM_CONFIGS_IMPORT_MAP = {
),
"XAIChatConfig": (".llms.xai.chat.transformation", "XAIChatConfig"),
"ZAIChatConfig": (".llms.zai.chat.transformation", "ZAIChatConfig"),
"ConsusChatConfig": (".llms.consus.chat.transformation", "ConsusChatConfig"),
"AIMLChatConfig": (".llms.aiml.chat.transformation", "AIMLChatConfig"),
"VolcEngineChatConfig": (
".llms.volcengine.chat.transformation",

View file

@ -540,6 +540,7 @@ LITELLM_CHAT_PROVIDERS = [
"openai",
"openai_like",
"bytez",
"consus",
"xai",
"custom_openai",
"text-completion-openai",
@ -783,6 +784,7 @@ openai_compatible_endpoints: List = [
"https://ai-gateway.vercel.sh/v1",
"https://api.inference.wandb.ai/v1",
"https://api.clarifai.com/v2/ext/openai/v1",
"https://api.consus.io/v1",
]
@ -802,6 +804,7 @@ openai_compatible_providers: List = [
"perplexity",
"xinference",
"xai",
"consus",
"zai",
"together_ai",
"fireworks_ai",

View file

@ -362,6 +362,9 @@ def get_llm_provider( # noqa: PLR0915
elif endpoint == "https://api.inference.wandb.ai/v1":
custom_llm_provider = "wandb"
dynamic_api_key = get_secret_str("WANDB_API_KEY")
elif endpoint == "https://api.consus.io/v1":
custom_llm_provider = "consus"
dynamic_api_key = get_secret_str("CONSUS_API_KEY")
if api_base is not None and not isinstance(api_base, str):
raise Exception(
@ -477,6 +480,8 @@ def get_llm_provider( # noqa: PLR0915
# bytez models
elif model.startswith("bytez/"):
custom_llm_provider = "bytez"
elif model.startswith("consus/"):
custom_llm_provider = "consus"
elif model.startswith("lemonade/"):
custom_llm_provider = "lemonade"
elif model.startswith("heroku/"):
@ -793,6 +798,13 @@ def _get_openai_compatible_provider_info( # noqa: PLR0915
) = litellm.XAIChatConfig()._get_openai_compatible_provider_info(
api_base, api_key
)
elif custom_llm_provider == "consus":
(
api_base,
dynamic_api_key,
) = litellm.ConsusChatConfig()._get_openai_compatible_provider_info(
api_base, api_key
)
elif custom_llm_provider == "zai":
(
api_base,

View file

View file

View file

@ -0,0 +1,55 @@
"""
Translates from OpenAI's `/v1/chat/completions` to Consus Gateway's
`/v1/chat/completions`.
Consus Gateway is OpenAI-compatible in every respect except authentication:
it requires the API key in an `x-api-key` header instead of
`Authorization: Bearer <key>`.
"""
from typing import List, Optional, Tuple
import litellm
from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import AllMessageValues
CONSUS_API_BASE = "https://api.consus.io/v1"
class ConsusChatConfig(OpenAIGPTConfig):
@staticmethod
def _resolve_api_key(api_key: Optional[str]) -> Optional[str]:
return api_key or litellm.consus_key or get_secret_str("CONSUS_API_KEY")
def _get_openai_compatible_provider_info(
self, api_base: Optional[str], api_key: Optional[str]
) -> Tuple[Optional[str], Optional[str]]:
api_base = api_base or get_secret_str("CONSUS_API_BASE") or CONSUS_API_BASE
dynamic_api_key = ConsusChatConfig._resolve_api_key(api_key)
return api_base, dynamic_api_key
def validate_environment(
self,
headers: dict,
model: str,
messages: List[AllMessageValues],
optional_params: dict,
litellm_params: dict,
api_key: Optional[str] = None,
api_base: Optional[str] = None,
) -> dict:
resolved_key = ConsusChatConfig._resolve_api_key(api_key)
if not resolved_key:
raise ValueError(
"Missing Consus API key. Set the CONSUS_API_KEY environment "
"variable, set litellm.consus_key, or pass api_key=... to "
"completion()."
)
headers["x-api-key"] = resolved_key
if "content-type" not in headers and "Content-Type" not in headers:
headers["Content-Type"] = "application/json"
return headers

View file

@ -2299,6 +2299,37 @@ def completion( # type: ignore # noqa: PLR0915
additional_args={"headers": headers},
)
raise e
elif custom_llm_provider == "consus":
## COMPLETION CALL
try:
response = base_llm_http_handler.completion(
model=model,
messages=messages,
headers=headers,
model_response=model_response,
api_key=api_key,
api_base=api_base,
acompletion=acompletion,
logging_obj=logging,
optional_params=optional_params,
litellm_params=litellm_params,
shared_session=shared_session,
timeout=timeout, # type: ignore
client=client,
custom_llm_provider=custom_llm_provider,
encoding=_get_encoding(),
stream=stream,
provider_config=provider_config,
)
except Exception as e:
## LOGGING - log the original exception returned
logging.post_call(
input=messages,
api_key=api_key,
original_response=str(e),
additional_args={"headers": headers},
)
raise e
elif custom_llm_provider == "groq":
api_base = (
api_base # for deepinfra/perplexity/anyscale/groq/friendliai we check in get_llm_provider and pass in the api base from there

View file

@ -12417,6 +12417,242 @@
"supports_tool_choice": true,
"supports_function_calling": true
},
"consus/claude-3-7-sonnet:il5+itar": {
"litellm_provider": "consus",
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"max_tokens": 64000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/claude-sonnet-4-5:il5+itar": {
"litellm_provider": "consus",
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"max_tokens": 64000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/claude-sonnet-4-5:il2": {
"litellm_provider": "consus",
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"max_tokens": 64000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/claude-opus-4-6:il2": {
"litellm_provider": "consus",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/claude-sonnet-4-6:il2": {
"litellm_provider": "consus",
"max_input_tokens": 1000000,
"max_output_tokens": 64000,
"max_tokens": 64000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/claude-opus-4-5:il2": {
"litellm_provider": "consus",
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"max_tokens": 64000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/claude-haiku-4-5:il2": {
"litellm_provider": "consus",
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"max_tokens": 64000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/claude-opus-4-1:il2": {
"litellm_provider": "consus",
"max_input_tokens": 200000,
"max_output_tokens": 32000,
"max_tokens": 32000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/claude-opus-4:il2": {
"litellm_provider": "consus",
"max_input_tokens": 200000,
"max_output_tokens": 32000,
"max_tokens": 32000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/claude-sonnet-4:il2": {
"litellm_provider": "consus",
"max_input_tokens": 1000000,
"max_output_tokens": 64000,
"max_tokens": 64000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/gemini-2-5-pro:il5": {
"litellm_provider": "consus",
"max_input_tokens": 1048576,
"max_output_tokens": 65535,
"max_tokens": 65535,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/gemini-2-5-flash:il5": {
"litellm_provider": "consus",
"max_input_tokens": 1048576,
"max_output_tokens": 65535,
"max_tokens": 65535,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/gpt-4.1:il5+itar": {
"litellm_provider": "consus",
"max_input_tokens": 300000,
"max_output_tokens": 32768,
"max_tokens": 32768,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/gpt-4.1-mini:il5+itar": {
"litellm_provider": "consus",
"max_input_tokens": 300000,
"max_output_tokens": 32768,
"max_tokens": 32768,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/gpt-5.1:il5+itar": {
"litellm_provider": "consus",
"max_input_tokens": 272000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true
},
"deepseek/deepseek-chat": {
"cache_creation_input_token_cost": 0.0,
"cache_read_input_token_cost": 2.8e-08,

View file

@ -3206,6 +3206,7 @@ class LlmProviders(str, Enum):
JINA_AI = "jina_ai"
XAI = "xai"
ZAI = "zai"
CONSUS = "consus"
CUSTOM_OPENAI = "custom_openai"
TEXT_COMPLETION_OPENAI = "text-completion-openai"
COHERE = "cohere"

View file

@ -8120,6 +8120,7 @@ class ProviderConfigManager:
LlmProviders.DATABRICKS: (lambda: litellm.DatabricksConfig(), False),
LlmProviders.XAI: (lambda: litellm.XAIChatConfig(), False),
LlmProviders.ZAI: (lambda: litellm.ZAIChatConfig(), False),
LlmProviders.CONSUS: (lambda: litellm.ConsusChatConfig(), False),
LlmProviders.LAMBDA_AI: (lambda: litellm.LambdaAIChatConfig(), False),
LlmProviders.LLAMA: (lambda: litellm.LlamaAPIConfig(), False),
LlmProviders.TEXT_COMPLETION_OPENAI: (

View file

@ -12422,6 +12422,242 @@
"supports_tool_choice": true,
"supports_function_calling": true
},
"consus/claude-3-7-sonnet:il5+itar": {
"litellm_provider": "consus",
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"max_tokens": 64000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/claude-sonnet-4-5:il5+itar": {
"litellm_provider": "consus",
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"max_tokens": 64000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/claude-sonnet-4-5:il2": {
"litellm_provider": "consus",
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"max_tokens": 64000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/claude-opus-4-6:il2": {
"litellm_provider": "consus",
"max_input_tokens": 1000000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/claude-sonnet-4-6:il2": {
"litellm_provider": "consus",
"max_input_tokens": 1000000,
"max_output_tokens": 64000,
"max_tokens": 64000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/claude-opus-4-5:il2": {
"litellm_provider": "consus",
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"max_tokens": 64000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/claude-haiku-4-5:il2": {
"litellm_provider": "consus",
"max_input_tokens": 200000,
"max_output_tokens": 64000,
"max_tokens": 64000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/claude-opus-4-1:il2": {
"litellm_provider": "consus",
"max_input_tokens": 200000,
"max_output_tokens": 32000,
"max_tokens": 32000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/claude-opus-4:il2": {
"litellm_provider": "consus",
"max_input_tokens": 200000,
"max_output_tokens": 32000,
"max_tokens": 32000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/claude-sonnet-4:il2": {
"litellm_provider": "consus",
"max_input_tokens": 1000000,
"max_output_tokens": 64000,
"max_tokens": 64000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/gemini-2-5-pro:il5": {
"litellm_provider": "consus",
"max_input_tokens": 1048576,
"max_output_tokens": 65535,
"max_tokens": 65535,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/gemini-2-5-flash:il5": {
"litellm_provider": "consus",
"max_input_tokens": 1048576,
"max_output_tokens": 65535,
"max_tokens": 65535,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/gpt-4.1:il5+itar": {
"litellm_provider": "consus",
"max_input_tokens": 300000,
"max_output_tokens": 32768,
"max_tokens": 32768,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/gpt-4.1-mini:il5+itar": {
"litellm_provider": "consus",
"max_input_tokens": 300000,
"max_output_tokens": 32768,
"max_tokens": 32768,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true
},
"consus/gpt-5.1:il5+itar": {
"litellm_provider": "consus",
"max_input_tokens": 272000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"input_cost_per_token": 0.0,
"output_cost_per_token": 0.0,
"supports_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true
},
"deepseek/deepseek-chat": {
"cache_creation_input_token_cost": 0.0,
"cache_read_input_token_cost": 2.8e-08,

View file

@ -0,0 +1,143 @@
import os
import sys
import pytest
sys.path.insert(0, os.path.abspath("../../../../.."))
import litellm
from litellm.llms.consus.chat.transformation import (
CONSUS_API_BASE,
ConsusChatConfig,
)
TEST_MESSAGES = [{"role": "user", "content": "Hello"}]
@pytest.fixture(autouse=True)
def _clear_consus_env(monkeypatch):
monkeypatch.delenv("CONSUS_API_KEY", raising=False)
monkeypatch.delenv("CONSUS_API_BASE", raising=False)
monkeypatch.setattr(litellm, "consus_key", None, raising=False)
yield
class TestConsusChatConfigAuth:
def test_validate_environment_uses_x_api_key(self):
config = ConsusChatConfig()
headers = config.validate_environment(
headers={},
model="claude-sonnet-4-5:il2",
messages=TEST_MESSAGES, # type: ignore
optional_params={},
litellm_params={},
api_key="test-key",
api_base=CONSUS_API_BASE,
)
assert headers["x-api-key"] == "test-key"
assert "Authorization" not in headers
assert headers["Content-Type"] == "application/json"
def test_validate_environment_raises_when_no_key(self):
config = ConsusChatConfig()
with pytest.raises(ValueError, match="Missing Consus API key"):
config.validate_environment(
headers={},
model="claude-sonnet-4-5:il2",
messages=TEST_MESSAGES, # type: ignore
optional_params={},
litellm_params={},
api_key=None,
api_base=CONSUS_API_BASE,
)
def test_validate_environment_resolves_from_env(self, monkeypatch):
monkeypatch.setenv("CONSUS_API_KEY", "env-key")
config = ConsusChatConfig()
headers = config.validate_environment(
headers={},
model="claude-sonnet-4-5:il2",
messages=TEST_MESSAGES, # type: ignore
optional_params={},
litellm_params={},
api_key=None,
api_base=CONSUS_API_BASE,
)
assert headers["x-api-key"] == "env-key"
def test_validate_environment_resolves_from_litellm_module(self, monkeypatch):
monkeypatch.setattr(litellm, "consus_key", "module-key", raising=False)
config = ConsusChatConfig()
headers = config.validate_environment(
headers={},
model="claude-sonnet-4-5:il2",
messages=TEST_MESSAGES, # type: ignore
optional_params={},
litellm_params={},
api_key=None,
api_base=CONSUS_API_BASE,
)
assert headers["x-api-key"] == "module-key"
def test_validate_environment_arg_takes_precedence(self, monkeypatch):
monkeypatch.setenv("CONSUS_API_KEY", "env-key")
monkeypatch.setattr(litellm, "consus_key", "module-key", raising=False)
config = ConsusChatConfig()
headers = config.validate_environment(
headers={},
model="claude-sonnet-4-5:il2",
messages=TEST_MESSAGES, # type: ignore
optional_params={},
litellm_params={},
api_key="arg-key",
api_base=CONSUS_API_BASE,
)
assert headers["x-api-key"] == "arg-key"
class TestConsusProviderInfo:
def test_default_api_base(self):
config = ConsusChatConfig()
api_base, _ = config._get_openai_compatible_provider_info(None, "k")
assert api_base == CONSUS_API_BASE
assert api_base == "https://api.consus.io/v1"
def test_explicit_api_base_wins(self):
config = ConsusChatConfig()
api_base, _ = config._get_openai_compatible_provider_info(
"https://other.example/v1", "k"
)
assert api_base == "https://other.example/v1"
def test_env_api_base_used_when_arg_none(self, monkeypatch):
monkeypatch.setenv("CONSUS_API_BASE", "https://staging.consus.io/v1")
config = ConsusChatConfig()
api_base, _ = config._get_openai_compatible_provider_info(None, "k")
assert api_base == "https://staging.consus.io/v1"
class TestConsusModelRouting:
def test_get_llm_provider_strips_prefix_keeps_colon(self, monkeypatch):
monkeypatch.setenv("CONSUS_API_KEY", "test-key")
model, provider, api_key, api_base = litellm.get_llm_provider(
"consus/claude-sonnet-4-5:il5+itar"
)
assert provider == "consus"
# The compliance suffix `:il5+itar` must be preserved end-to-end —
# only the leading `consus/` prefix is stripped.
assert model == "claude-sonnet-4-5:il5+itar"
assert api_base == "https://api.consus.io/v1"
assert api_key == "test-key"
def test_get_llm_provider_for_il2_models(self, monkeypatch):
monkeypatch.setenv("CONSUS_API_KEY", "test-key")
model, provider, _, _ = litellm.get_llm_provider("consus/claude-opus-4-6:il2")
assert provider == "consus"
assert model == "claude-opus-4-6:il2"
def test_get_llm_provider_for_gemini(self, monkeypatch):
monkeypatch.setenv("CONSUS_API_KEY", "test-key")
model, provider, _, _ = litellm.get_llm_provider("consus/gemini-2-5-pro:il5")
assert provider == "consus"
assert model == "gemini-2-5-pro:il5"

View file

@ -0,0 +1,104 @@
"""End-to-end (mocked HTTP) tests for the Consus provider.
Verifies that `litellm.completion(model="consus/...", ...)`:
1. Hits `https://api.consus.io/v1/chat/completions`
2. Sends the API key in `x-api-key` (NOT `Authorization`)
3. Forwards the model name unprefixed (consus/X -> X) to the gateway
4. Returns a parsed OpenAI-style response
"""
import json
import os
import sys
import pytest
sys.path.insert(0, os.path.abspath("../../../../.."))
import litellm
from litellm.llms.consus.chat.transformation import CONSUS_API_BASE
TEST_API_KEY = "consus-test-key"
TEST_MODEL_NAME = "claude-sonnet-4-5:il5+itar"
TEST_MODEL = f"consus/{TEST_MODEL_NAME}"
@pytest.fixture(autouse=True)
def _clear_consus_env(monkeypatch):
monkeypatch.delenv("CONSUS_API_KEY", raising=False)
monkeypatch.delenv("CONSUS_API_BASE", raising=False)
monkeypatch.setattr(litellm, "consus_key", None, raising=False)
yield
def _openai_style_response(content: str) -> dict:
return {
"id": "chatcmpl-consus-1",
"object": "chat.completion",
"created": 1234567890,
"model": TEST_MODEL_NAME,
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": content},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
}
class TestConsusCompletionMock:
def test_request_uses_x_api_key_and_unprefixed_model(self, respx_mock):
route = respx_mock.post(f"{CONSUS_API_BASE}/chat/completions").respond(
json=_openai_style_response("hello from consus"),
status_code=200,
)
response = litellm.completion(
model=TEST_MODEL,
messages=[{"role": "user", "content": "ping"}],
api_key=TEST_API_KEY,
)
assert response.choices[0].message.content == "hello from consus" # type: ignore
assert route.called
request = route.calls[0].request
assert request.headers["x-api-key"] == TEST_API_KEY
assert "authorization" not in {k.lower() for k in request.headers.keys()}
sent = json.loads(request.content)
# The `consus/` prefix must be stripped before forwarding —
# but the `:compliance` suffix must be preserved.
assert sent["model"] == TEST_MODEL_NAME
def test_request_uses_default_api_base_when_none_given(self, respx_mock):
route = respx_mock.post(f"{CONSUS_API_BASE}/chat/completions").respond(
json=_openai_style_response("ok"),
status_code=200,
)
litellm.completion(
model="consus/gpt-4.1:il5+itar",
messages=[{"role": "user", "content": "hi"}],
api_key=TEST_API_KEY,
)
assert route.called
def test_completion_uses_env_api_key(self, monkeypatch, respx_mock):
monkeypatch.setenv("CONSUS_API_KEY", "env-resolved-key")
route = respx_mock.post(f"{CONSUS_API_BASE}/chat/completions").respond(
json=_openai_style_response("ok"),
status_code=200,
)
litellm.completion(
model=TEST_MODEL,
messages=[{"role": "user", "content": "hi"}],
)
assert route.called
assert route.calls[0].request.headers["x-api-key"] == "env-resolved-key"