From cc0dfb01bd9223210bf9f67e5092f9c3203b45ab Mon Sep 17 00:00:00 2001 From: ericmagliarditi <188399812+ericMConsus@users.noreply.github.com> Date: Thu, 7 May 2026 15:47:54 -0400 Subject: [PATCH] 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 --- litellm/__init__.py | 9 + litellm/_lazy_imports_registry.py | 2 + litellm/constants.py | 3 + .../get_llm_provider_logic.py | 12 + litellm/llms/consus/__init__.py | 0 litellm/llms/consus/chat/__init__.py | 0 litellm/llms/consus/chat/transformation.py | 55 ++++ litellm/main.py | 31 +++ ...odel_prices_and_context_window_backup.json | 236 ++++++++++++++++++ litellm/types/utils.py | 1 + litellm/utils.py | 1 + model_prices_and_context_window.json | 236 ++++++++++++++++++ tests/test_litellm/llms/consus/__init__.py | 0 .../test_litellm/llms/consus/chat/__init__.py | 0 .../chat/test_consus_chat_transformation.py | 143 +++++++++++ .../consus/chat/test_consus_completion.py | 104 ++++++++ 16 files changed, 833 insertions(+) create mode 100644 litellm/llms/consus/__init__.py create mode 100644 litellm/llms/consus/chat/__init__.py create mode 100644 litellm/llms/consus/chat/transformation.py create mode 100644 tests/test_litellm/llms/consus/__init__.py create mode 100644 tests/test_litellm/llms/consus/chat/__init__.py create mode 100644 tests/test_litellm/llms/consus/chat/test_consus_chat_transformation.py create mode 100644 tests/test_litellm/llms/consus/chat/test_consus_completion.py diff --git a/litellm/__init__.py b/litellm/__init__.py index e1b367fb234..c20757e5085 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -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 ( diff --git a/litellm/_lazy_imports_registry.py b/litellm/_lazy_imports_registry.py index 3531e8d96b9..dc7b550eaa6 100644 --- a/litellm/_lazy_imports_registry.py +++ b/litellm/_lazy_imports_registry.py @@ -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", diff --git a/litellm/constants.py b/litellm/constants.py index 072c2c358f7..55c3452bdce 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -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", diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index ba6d438f16c..72520ead097 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -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, diff --git a/litellm/llms/consus/__init__.py b/litellm/llms/consus/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/litellm/llms/consus/chat/__init__.py b/litellm/llms/consus/chat/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/litellm/llms/consus/chat/transformation.py b/litellm/llms/consus/chat/transformation.py new file mode 100644 index 00000000000..b8720f787f3 --- /dev/null +++ b/litellm/llms/consus/chat/transformation.py @@ -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 `. +""" + +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 diff --git a/litellm/main.py b/litellm/main.py index c3d1c2e05b0..1230b1fb31a 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -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 diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 8df25d2c9b5..533e8dea436 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -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, diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 400edcac889..01bdcee4fe8 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -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" diff --git a/litellm/utils.py b/litellm/utils.py index da80e4ae164..77b1ad9aeae 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -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: ( diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 70b065aa918..3455aca2f08 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -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, diff --git a/tests/test_litellm/llms/consus/__init__.py b/tests/test_litellm/llms/consus/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_litellm/llms/consus/chat/__init__.py b/tests/test_litellm/llms/consus/chat/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_litellm/llms/consus/chat/test_consus_chat_transformation.py b/tests/test_litellm/llms/consus/chat/test_consus_chat_transformation.py new file mode 100644 index 00000000000..2d8423b151d --- /dev/null +++ b/tests/test_litellm/llms/consus/chat/test_consus_chat_transformation.py @@ -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" diff --git a/tests/test_litellm/llms/consus/chat/test_consus_completion.py b/tests/test_litellm/llms/consus/chat/test_consus_completion.py new file mode 100644 index 00000000000..e5b928c0cf7 --- /dev/null +++ b/tests/test_litellm/llms/consus/chat/test_consus_completion.py @@ -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"