From d82defad622ea382c753d0748a30d84e758ec6ee Mon Sep 17 00:00:00 2001 From: "s.agrawal" Date: Sat, 15 Aug 2026 00:31:44 +0530 Subject: [PATCH] feat(melious): add Melious as a provider Melious is a European AI gateway fronting open-weight models, serving an OpenAI-compatible endpoint and an Anthropic-compatible Messages API from the same host. MeliousChatConfig routes /v1/chat/completions and MeliousAnthropicMessagesConfig routes /v1/messages, so both surfaces resolve from one model prefix and one MELIOUS_API_KEY instead of a hand-wired api_base per deployment. /v1/responses is inherited from the chat-completions bridge, as it is for the other openai-compatible providers Base-URL normalization lives in _get_openai_compatible_provider_info rather than in get_complete_url, where the sibling dual-compatible providers put it. Chat requests reach Melious through the OpenAI SDK path, which consumes api_base directly and never calls get_complete_url, so an override there would not have protected the route that actually runs. Resolving once at provider-info time covers the SDK path and the httpx handler both, and it lets a caller pass the bare root, the /v1 root, or a full endpoint URL for either surface The melious branch in _get_openai_compatible_provider_info binds new names and returns early instead of rebinding the function's parameters the way the surrounding branches do. That keeps the parameter-rebinding count flat against the type-discipline budget, and it matches the early-return shape already used by the aiohttp_openai branch Cost-map entries cover 17 flagship chat models. Context lengths, capability flags, and cache-read rates are taken from the provider's own model metadata endpoint rather than hand-copied. The published rates are EUR per million tokens written into LiteLLM's USD-per-token fields, which is the same convention the existing ovhcloud entries already use for OVHcloud's EUR list prices Tests cover URL normalization for every base-URL shape either surface accepts, argument-over-environment precedence for the key and the base, provider and config resolution through ProviderConfigManager, the billing-metadata strip that Melious requires on the Anthropic surface, and cost lookup against the shipped map --- README.md | 1 + litellm/__init__.py | 10 + litellm/_lazy_imports_registry.py | 2 + litellm/constants.py | 2 + litellm/cost_calculator.py | 5 + .../get_llm_provider_logic.py | 7 + .../get_supported_openai_params.py | 2 + litellm/llms/melious/__init__.py | 0 litellm/llms/melious/chat/__init__.py | 0 litellm/llms/melious/chat/transformation.py | 23 ++ litellm/llms/melious/common_utils.py | 31 +++ litellm/llms/melious/cost_calculator.py | 6 + litellm/llms/melious/messages/__init__.py | 0 .../llms/melious/messages/transformation.py | 63 +++++ ...odel_prices_and_context_window_backup.json | 253 ++++++++++++++++++ litellm/types/utils.py | 1 + litellm/utils.py | 19 ++ model_prices_and_context_window.json | 253 ++++++++++++++++++ provider_endpoints_support.json | 18 ++ tests/test_litellm/llms/melious/__init__.py | 0 .../llms/melious/chat/__init__.py | 0 .../chat/test_melious_chat_transformation.py | 130 +++++++++ .../llms/melious/messages/__init__.py | 0 ...lious_anthropic_messages_transformation.py | 148 ++++++++++ .../llms/melious/test_common_utils.py | 51 ++++ .../llms/melious/test_cost_calculator.py | 51 ++++ 26 files changed, 1076 insertions(+) create mode 100644 litellm/llms/melious/__init__.py create mode 100644 litellm/llms/melious/chat/__init__.py create mode 100644 litellm/llms/melious/chat/transformation.py create mode 100644 litellm/llms/melious/common_utils.py create mode 100644 litellm/llms/melious/cost_calculator.py create mode 100644 litellm/llms/melious/messages/__init__.py create mode 100644 litellm/llms/melious/messages/transformation.py create mode 100644 tests/test_litellm/llms/melious/__init__.py create mode 100644 tests/test_litellm/llms/melious/chat/__init__.py create mode 100644 tests/test_litellm/llms/melious/chat/test_melious_chat_transformation.py create mode 100644 tests/test_litellm/llms/melious/messages/__init__.py create mode 100644 tests/test_litellm/llms/melious/messages/test_melious_anthropic_messages_transformation.py create mode 100644 tests/test_litellm/llms/melious/test_common_utils.py create mode 100644 tests/test_litellm/llms/melious/test_cost_calculator.py diff --git a/README.md b/README.md index 32b0160dbaa..421c2a5d46d 100644 --- a/README.md +++ b/README.md @@ -331,6 +331,7 @@ curl -X POST 'http://0.0.0.0:4000/v1/chat/completions' \ | [Llamafile (`llamafile`)](https://docs.litellm.ai/docs/providers/llamafile) | ✅ | ✅ | ✅ | | | | | | | | | [LM Studio (`lm_studio`)](https://docs.litellm.ai/docs/providers/lm_studio) | ✅ | ✅ | ✅ | | | | | | | | | [Maritalk (`maritalk`)](https://docs.litellm.ai/docs/providers/maritalk) | ✅ | ✅ | ✅ | | | | | | | | +| [Melious (`melious`)](https://docs.litellm.ai/docs/providers/melious) | ✅ | ✅ | ✅ | | | | | | | | | [Meta - Llama API (`meta_llama`)](https://docs.litellm.ai/docs/providers/meta_llama) | ✅ | ✅ | ✅ | | | | | | | | | [Mistral AI API (`mistral`)](https://docs.litellm.ai/docs/providers/mistral) | ✅ | ✅ | ✅ | ✅ | | | | | | | | [ModelScope (`modelscope`)](https://docs.litellm.ai/docs/providers/modelscope) | ✅ | ✅ | ✅ | | ✅ | | | | | | diff --git a/litellm/__init__.py b/litellm/__init__.py index 056dd532f5f..6cd214dc9ee 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -305,6 +305,7 @@ lemonade_key: Optional[str] = None sap_service_key: Optional[str] = None amazon_nova_api_key: Optional[str] = None inception_key: Optional[str] = None +melious_key: Optional[str] = None common_cloud_provider_auth_params: dict = { "params": ["project", "region_name", "token"], "providers": ["vertex_ai", "bedrock", "watsonx", "azure", "vertex_ai_beta"], @@ -607,6 +608,7 @@ xai_models: Set = set() zai_models: Set = set() deepseek_models: Set = set() tencent_models: Set = set() +melious_models: Set = set() # mutable-ok: add_known_models() populates this from the cost map runwayml_models: Set = set() azure_ai_models: Set = set() jina_ai_models: Set = set() @@ -821,6 +823,8 @@ def _populate_provider_model_sets(model_cost_map: Dict) -> None: deepseek_models.add(key) elif value.get("litellm_provider") == "tencent": tencent_models.add(key) + elif value.get("litellm_provider") == "melious": + melious_models.add(key) elif value.get("litellm_provider") == "runwayml": runwayml_models.add(key) elif value.get("litellm_provider") == "meta_llama": @@ -1074,6 +1078,7 @@ model_list = list( | docker_model_runner_models | reducto_models | bedrock_mantle_models + | melious_models | set(clarifai_models) ) @@ -1124,6 +1129,7 @@ def _build_models_by_provider() -> dict: "fal_ai": fal_ai_models, "deepseek": deepseek_models, "tencent": tencent_models, + "melious": melious_models, "runwayml": runwayml_models, "mistral": mistral_chat_models, "azure_ai": azure_ai_models, @@ -1841,6 +1847,9 @@ if TYPE_CHECKING: from .llms.tencent.chat.transformation import ( TencentChatConfig as _TencentChatConfig, ) + from .llms.melious.chat.transformation import ( + MeliousChatConfig as _MeliousChatConfig, + ) from .llms.sap.chat.transformation import ( GenAIHubOrchestrationConfig as _GenAIHubOrchestrationConfig, ) @@ -1884,6 +1893,7 @@ if TYPE_CHECKING: VLLMConfig: Type[_VLLMConfig] DeepSeekChatConfig: Type[_DeepSeekChatConfig] TencentChatConfig: Type[_TencentChatConfig] + MeliousChatConfig: Type[_MeliousChatConfig] GenAIHubOrchestrationConfig: Type[_GenAIHubOrchestrationConfig] GenAIHubEmbeddingConfig: Type[_GenAIHubEmbeddingConfig] AzureOpenAIO1Config: Type[_AzureOpenAIO1Config] diff --git a/litellm/_lazy_imports_registry.py b/litellm/_lazy_imports_registry.py index 89c72acc06d..d46f51b28ef 100644 --- a/litellm/_lazy_imports_registry.py +++ b/litellm/_lazy_imports_registry.py @@ -287,6 +287,7 @@ LLM_CONFIG_NAMES: Final = ( "VLLMConfig", "DeepSeekChatConfig", "TencentChatConfig", + "MeliousChatConfig", "LMStudioChatConfig", "LmStudioEmbeddingConfig", "NscaleConfig", @@ -1100,6 +1101,7 @@ _LLM_CONFIGS_IMPORT_MAP: Final = { "VLLMConfig": (".llms.vllm.completion.transformation", "VLLMConfig"), "DeepSeekChatConfig": (".llms.deepseek.chat.transformation", "DeepSeekChatConfig"), "TencentChatConfig": (".llms.tencent.chat.transformation", "TencentChatConfig"), + "MeliousChatConfig": (".llms.melious.chat.transformation", "MeliousChatConfig"), "LMStudioChatConfig": (".llms.lm_studio.chat.transformation", "LMStudioChatConfig"), "LmStudioEmbeddingConfig": ( ".llms.lm_studio.embed.transformation", diff --git a/litellm/constants.py b/litellm/constants.py index 6449834d6a4..47e57c5a6d4 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -589,6 +589,7 @@ LITELLM_CHAT_PROVIDERS: Final = [ "lemonade", "docker_model_runner", "amazon_nova", + "melious", ] LITELLM_EMBEDDING_PROVIDERS_SUPPORTING_INPUT_ARRAY_OF_TOKENS: Final = [ @@ -770,6 +771,7 @@ openai_compatible_providers: Final[list] = [ "codestral", "deepseek", "tencent", + "melious", "deepinfra", "perplexity", "xinference", diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 6b6653c5646..c6adfefb267 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -59,6 +59,9 @@ from litellm.llms.gemini.cost_calculator import cost_per_token as gemini_cost_pe from litellm.llms.lemonade.cost_calculator import ( cost_per_token as lemonade_cost_per_token, ) +from litellm.llms.melious.cost_calculator import ( + cost_per_token as melious_cost_per_token, +) from litellm.llms.openai.cost_calculation import ( _video_output_cost_per_second, ) @@ -622,6 +625,8 @@ def cost_per_token( return deepseek_cost_per_token(model=model, usage=usage_block) elif custom_llm_provider == "tencent": return tencent_cost_per_token(model=model, usage=usage_block) + elif custom_llm_provider == "melious": + return melious_cost_per_token(model=model, usage=usage_block) elif custom_llm_provider == "perplexity": return perplexity_cost_per_token(model=model, usage=usage_block) elif custom_llm_provider == "xai": diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index dbb40913e14..da5d0fe0f14 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -470,6 +470,8 @@ def get_llm_provider( custom_llm_provider = "amazon_nova" elif model.startswith("sap/"): custom_llm_provider = "sap" + elif model.startswith("melious/"): + custom_llm_provider = "melious" # rebind-ok: this if/elif chain resolves the provider in place # Last resort for an otherwise-unknown model: a declarative # fallback-generalization routing rule (e.g. routes future claude-* to anthropic). @@ -655,6 +657,11 @@ def _get_openai_compatible_provider_info( api_base = api_base or get_secret("TENCENT_API_BASE") or "https://tokenhub-intl.tencentcloudmaas.com/v1" dynamic_api_key = api_key or get_secret_str("TENCENT_API_KEY") + elif custom_llm_provider == "melious": + melious_api_base, melious_api_key = litellm.MeliousChatConfig()._get_openai_compatible_provider_info( + api_base, api_key + ) + return model, custom_llm_provider, melious_api_key, melious_api_base elif custom_llm_provider == "fireworks_ai": # fireworks is openai compatible, we just need to set this to custom_openai and have the api_base be https://api.fireworks.ai/inference/v1 ( diff --git a/litellm/litellm_core_utils/get_supported_openai_params.py b/litellm/litellm_core_utils/get_supported_openai_params.py index 72f36661f4c..edde2a00df8 100644 --- a/litellm/litellm_core_utils/get_supported_openai_params.py +++ b/litellm/litellm_core_utils/get_supported_openai_params.py @@ -108,6 +108,8 @@ def get_supported_openai_params( return litellm.DeepSeekChatConfig().get_supported_openai_params(model=model) elif custom_llm_provider == "tencent": return litellm.TencentChatConfig().get_supported_openai_params(model=model) + elif custom_llm_provider == "melious": + return litellm.MeliousChatConfig().get_supported_openai_params(model=model) elif custom_llm_provider == "cohere_chat" or custom_llm_provider == "cohere": return litellm.CohereChatConfig().get_supported_openai_params(model=model) elif custom_llm_provider == "maritalk": diff --git a/litellm/llms/melious/__init__.py b/litellm/llms/melious/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/litellm/llms/melious/chat/__init__.py b/litellm/llms/melious/chat/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/litellm/llms/melious/chat/transformation.py b/litellm/llms/melious/chat/transformation.py new file mode 100644 index 00000000000..4f382d943e1 --- /dev/null +++ b/litellm/llms/melious/chat/transformation.py @@ -0,0 +1,23 @@ +""" +Translates from OpenAI's `/v1/chat/completions` to Melious' OpenAI-compatible endpoint. + +Melious is a European AI gateway (https://melious.ai) fronting open-weight models. +""" + +from typing import Final + +from litellm.secret_managers.main import get_secret_str + +from ...openai.chat.gpt_transformation import OpenAIGPTConfig +from ..common_utils import MELIOUS_OPENAI_API_BASE, openai_api_base + + +class MeliousChatConfig(OpenAIGPTConfig): + @property + def custom_llm_provider(self) -> str | None: + return "melious" + + def _get_openai_compatible_provider_info(self, api_base: str | None, api_key: str | None) -> tuple[str, str | None]: + resolved_api_base: Final = api_base or get_secret_str("MELIOUS_API_BASE") or MELIOUS_OPENAI_API_BASE + resolved_api_key: Final = api_key or get_secret_str("MELIOUS_API_KEY") + return openai_api_base(resolved_api_base), resolved_api_key diff --git a/litellm/llms/melious/common_utils.py b/litellm/llms/melious/common_utils.py new file mode 100644 index 00000000000..1701b2da121 --- /dev/null +++ b/litellm/llms/melious/common_utils.py @@ -0,0 +1,31 @@ +""" +Shared base-URL handling for Melious. + +Melious serves the OpenAI-compatible surface under ``/v1`` and the +Anthropic-compatible Messages API under the same root, so both configs resolve +``MELIOUS_API_BASE`` and normalize whatever shape the caller supplied. +""" + +from typing import Final + +MELIOUS_API_BASE: Final = "https://api.melious.ai" +MELIOUS_OPENAI_API_BASE: Final = f"{MELIOUS_API_BASE}/v1" + +_CHAT_COMPLETIONS_SUFFIX: Final = "/chat/completions" +_MESSAGES_SUFFIX: Final = "/v1/messages" +_VERSION_SUFFIX: Final = "/v1" + + +def openai_api_base(api_base: str) -> str: + """``{root}/v1``, whether the caller passed the root, ``/v1``, or the full chat URL.""" + trimmed: Final = api_base.rstrip("/").removesuffix(_CHAT_COMPLETIONS_SUFFIX) + return trimmed if trimmed.endswith(_VERSION_SUFFIX) else f"{trimmed}{_VERSION_SUFFIX}" + + +def anthropic_messages_url(api_base: str) -> str: + """``{root}/v1/messages``, whether the caller passed the root, ``/v1``, or the full URL.""" + trimmed: Final = api_base.rstrip("/") + if trimmed.endswith(_MESSAGES_SUFFIX): + return trimmed + root: Final = trimmed.removesuffix(_CHAT_COMPLETIONS_SUFFIX).removesuffix(_VERSION_SUFFIX) + return f"{root}{_MESSAGES_SUFFIX}" diff --git a/litellm/llms/melious/cost_calculator.py b/litellm/llms/melious/cost_calculator.py new file mode 100644 index 00000000000..0f132672d73 --- /dev/null +++ b/litellm/llms/melious/cost_calculator.py @@ -0,0 +1,6 @@ +from litellm.litellm_core_utils.llm_cost_calc.utils import generic_cost_per_token +from litellm.types.utils import Usage + + +def cost_per_token(model: str, usage: Usage) -> tuple[float, float]: + return generic_cost_per_token(model=model, usage=usage, custom_llm_provider="melious") diff --git a/litellm/llms/melious/messages/__init__.py b/litellm/llms/melious/messages/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/litellm/llms/melious/messages/transformation.py b/litellm/llms/melious/messages/transformation.py new file mode 100644 index 00000000000..5b21b1a2684 --- /dev/null +++ b/litellm/llms/melious/messages/transformation.py @@ -0,0 +1,63 @@ +""" +Melious Anthropic-compatible messages transformation config. + +Melious serves the Anthropic Messages API at https://api.melious.ai/v1/messages +alongside its OpenAI-compatible endpoint, so the Anthropic payload is forwarded +untranslated. It rejects Anthropic's billing-attribution system blocks. +""" + +import litellm +from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( + AnthropicMessagesConfig, +) +from litellm.secret_managers.main import get_secret_str + +from ..common_utils import MELIOUS_API_BASE, anthropic_messages_url + + +class MeliousAnthropicMessagesConfig(AnthropicMessagesConfig): + @property + def custom_llm_provider(self) -> str | None: + return "melious" + + def should_strip_billing_metadata(self) -> bool: + return True + + @staticmethod + def _resolve_api_key(api_key: str | None) -> str | None: + return api_key or get_secret_str("MELIOUS_API_KEY") or litellm.api_key + + @staticmethod + def _resolve_api_base(api_base: str | None) -> str: + return api_base or get_secret_str("MELIOUS_API_BASE") or MELIOUS_API_BASE + + def validate_anthropic_messages_environment( + self, + headers: dict[str, str], # mutable-ok: BaseAnthropicMessagesConfig signature + model: str, + messages: list[object], # mutable-ok: BaseAnthropicMessagesConfig signature + optional_params: dict[str, object], # mutable-ok: BaseAnthropicMessagesConfig signature + litellm_params: dict[str, object], # mutable-ok: BaseAnthropicMessagesConfig signature + api_key: str | None = None, + api_base: str | None = None, + ) -> tuple[dict[str, str], str | None]: # mutable-ok: BaseAnthropicMessagesConfig signature + return super().validate_anthropic_messages_environment( + headers=headers, + model=model, + messages=messages, + optional_params=optional_params, + litellm_params=litellm_params, + api_key=self._resolve_api_key(api_key), + api_base=api_base, + ) + + def get_complete_url( + self, + api_base: str | None, + api_key: str | None, + model: str, + optional_params: dict[str, object], # mutable-ok: BaseAnthropicMessagesConfig signature + litellm_params: dict[str, object], # mutable-ok: BaseAnthropicMessagesConfig signature + stream: bool | None = None, + ) -> str: + return anthropic_messages_url(self._resolve_api_base(api_base)) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index b288269b0a2..12251668080 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -47438,6 +47438,259 @@ "supports_reasoning": true, "supports_vision": false }, + "melious/glm-5.2": { + "cache_read_input_token_cost": 2.5e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "melious", + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 4e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/glm-5.1": { + "cache_read_input_token_cost": 3.2e-07, + "input_cost_per_token": 1.3e-06, + "litellm_provider": "melious", + "max_input_tokens": 203000, + "max_output_tokens": 203000, + "max_tokens": 203000, + "mode": "chat", + "output_cost_per_token": 4.05e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/glm-5": { + "cache_read_input_token_cost": 2.3e-07, + "input_cost_per_token": 9.5e-07, + "litellm_provider": "melious", + "max_input_tokens": 203000, + "max_output_tokens": 203000, + "max_tokens": 203000, + "mode": "chat", + "output_cost_per_token": 2.95e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/glm-4.7": { + "input_cost_per_token": 5.5e-07, + "litellm_provider": "melious", + "max_input_tokens": 203000, + "max_output_tokens": 203000, + "max_tokens": 203000, + "mode": "chat", + "output_cost_per_token": 2.2e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/deepseek-v4-pro": { + "cache_read_input_token_cost": 4.1e-07, + "input_cost_per_token": 1.6e-06, + "litellm_provider": "melious", + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 3.2e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/deepseek-v4-flash": { + "cache_read_input_token_cost": 4e-08, + "input_cost_per_token": 1.5e-07, + "litellm_provider": "melious", + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 3e-07, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/kimi-k3": { + "cache_read_input_token_cost": 6.9e-07, + "input_cost_per_token": 2.75e-06, + "litellm_provider": "melious", + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 1.375e-05, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "melious/kimi-k2.7-code": { + "cache_read_input_token_cost": 1.9e-07, + "input_cost_per_token": 7e-07, + "litellm_provider": "melious", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 3e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "melious/qwen3.8-max": { + "cache_read_input_token_cost": 6e-07, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "melious", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 6e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/qwen3.5-397b-a17b": { + "input_cost_per_token": 6e-07, + "litellm_provider": "melious", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 3.6e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "melious/qwen3-coder-next": { + "input_cost_per_token": 1.7e-07, + "litellm_provider": "melious", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 9e-07, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "melious/minimax-m3": { + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 4e-07, + "litellm_provider": "melious", + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 2e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "melious/mistral-medium-3.5-128b": { + "input_cost_per_token": 1.5e-06, + "litellm_provider": "melious", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "melious/devstral-2-123b-instruct-2512": { + "input_cost_per_token": 4e-07, + "litellm_provider": "melious", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/gpt-oss-120b": { + "input_cost_per_token": 4e-08, + "litellm_provider": "melious", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2e-07, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/llama-3.3-70b-instruct": { + "input_cost_per_token": 6e-07, + "litellm_provider": "melious", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 6.5e-07, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/apertus-70b": { + "input_cost_per_token": 1.25e-06, + "litellm_provider": "melious", + "max_input_tokens": 65536, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 2e-06, + "source": "https://melious.ai/hub/models" + }, "pinstripes/ps/glm-4.5-air": { "max_tokens": 128000, "max_input_tokens": 128000, diff --git a/litellm/types/utils.py b/litellm/types/utils.py index d9ef538d530..dc29a66b9c8 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3719,6 +3719,7 @@ class LlmProviders(str, Enum): CURSOR = "cursor" BEDROCK_MANTLE = "bedrock_mantle" GDC = "gdc" + MELIOUS = "melious" # Create a set of all provider values for quick lookup diff --git a/litellm/utils.py b/litellm/utils.py index 79372f00284..d58d86424d1 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -4411,6 +4411,13 @@ def get_optional_params( model=model, drop_params=(drop_params if drop_params is not None and isinstance(drop_params, bool) else False), ) + elif custom_llm_provider == "melious": + optional_params = litellm.MeliousChatConfig().map_openai_params( + non_default_params=non_default_params, + optional_params=optional_params, + model=model, + drop_params=(drop_params if drop_params is not None and isinstance(drop_params, bool) else False), + ) elif custom_llm_provider == "openrouter": optional_params = litellm.OpenrouterConfig().map_openai_params( non_default_params=non_default_params, @@ -6272,6 +6279,11 @@ def validate_environment( keys_in_environment = True else: missing_keys.append("TENCENT_API_KEY") + elif custom_llm_provider == "melious": + if "MELIOUS_API_KEY" in os.environ: + keys_in_environment = True + else: + missing_keys.append("MELIOUS_API_KEY") elif custom_llm_provider == "mistral": if "MISTRAL_API_KEY" in os.environ: keys_in_environment = True @@ -7814,6 +7826,7 @@ class ProviderConfigManager: # Simple provider mappings (no model parameter needed) LlmProviders.DEEPSEEK: (lambda: litellm.DeepSeekChatConfig(), False), LlmProviders.TENCENT: (lambda: litellm.TencentChatConfig(), False), + LlmProviders.MELIOUS: (lambda: litellm.MeliousChatConfig(), False), LlmProviders.GROQ: (lambda: litellm.GroqChatConfig(), False), LlmProviders.BEDROCK_MANTLE: ( lambda: litellm.BedrockMantleChatConfig(), @@ -8258,6 +8271,12 @@ class ProviderConfigManager: ) return TencentAnthropicMessagesConfig() + elif litellm.LlmProviders.MELIOUS == provider: + from litellm.llms.melious.messages.transformation import ( + MeliousAnthropicMessagesConfig, + ) + + return MeliousAnthropicMessagesConfig() elif litellm.LlmProviders.GITHUB_COPILOT == provider: if "claude" in model_lower: from litellm.llms.github_copilot.messages.transformation import ( diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index b288269b0a2..12251668080 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -47438,6 +47438,259 @@ "supports_reasoning": true, "supports_vision": false }, + "melious/glm-5.2": { + "cache_read_input_token_cost": 2.5e-07, + "input_cost_per_token": 1e-06, + "litellm_provider": "melious", + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 4e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/glm-5.1": { + "cache_read_input_token_cost": 3.2e-07, + "input_cost_per_token": 1.3e-06, + "litellm_provider": "melious", + "max_input_tokens": 203000, + "max_output_tokens": 203000, + "max_tokens": 203000, + "mode": "chat", + "output_cost_per_token": 4.05e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/glm-5": { + "cache_read_input_token_cost": 2.3e-07, + "input_cost_per_token": 9.5e-07, + "litellm_provider": "melious", + "max_input_tokens": 203000, + "max_output_tokens": 203000, + "max_tokens": 203000, + "mode": "chat", + "output_cost_per_token": 2.95e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/glm-4.7": { + "input_cost_per_token": 5.5e-07, + "litellm_provider": "melious", + "max_input_tokens": 203000, + "max_output_tokens": 203000, + "max_tokens": 203000, + "mode": "chat", + "output_cost_per_token": 2.2e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/deepseek-v4-pro": { + "cache_read_input_token_cost": 4.1e-07, + "input_cost_per_token": 1.6e-06, + "litellm_provider": "melious", + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 3.2e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/deepseek-v4-flash": { + "cache_read_input_token_cost": 4e-08, + "input_cost_per_token": 1.5e-07, + "litellm_provider": "melious", + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 3e-07, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/kimi-k3": { + "cache_read_input_token_cost": 6.9e-07, + "input_cost_per_token": 2.75e-06, + "litellm_provider": "melious", + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 1.375e-05, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "melious/kimi-k2.7-code": { + "cache_read_input_token_cost": 1.9e-07, + "input_cost_per_token": 7e-07, + "litellm_provider": "melious", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 3e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "melious/qwen3.8-max": { + "cache_read_input_token_cost": 6e-07, + "input_cost_per_token": 2.5e-06, + "litellm_provider": "melious", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 6e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/qwen3.5-397b-a17b": { + "input_cost_per_token": 6e-07, + "litellm_provider": "melious", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 3.6e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "melious/qwen3-coder-next": { + "input_cost_per_token": 1.7e-07, + "litellm_provider": "melious", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 9e-07, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "melious/minimax-m3": { + "cache_read_input_token_cost": 1e-07, + "input_cost_per_token": 4e-07, + "litellm_provider": "melious", + "max_input_tokens": 1000000, + "max_output_tokens": 1000000, + "max_tokens": 1000000, + "mode": "chat", + "output_cost_per_token": 2e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "melious/mistral-medium-3.5-128b": { + "input_cost_per_token": 1.5e-06, + "litellm_provider": "melious", + "max_input_tokens": 256000, + "max_output_tokens": 256000, + "max_tokens": 256000, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "melious/devstral-2-123b-instruct-2512": { + "input_cost_per_token": 4e-07, + "litellm_provider": "melious", + "max_input_tokens": 200000, + "max_output_tokens": 8192, + "max_tokens": 8192, + "mode": "chat", + "output_cost_per_token": 2e-06, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/gpt-oss-120b": { + "input_cost_per_token": 4e-08, + "litellm_provider": "melious", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 2e-07, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/llama-3.3-70b-instruct": { + "input_cost_per_token": 6e-07, + "litellm_provider": "melious", + "max_input_tokens": 131072, + "max_output_tokens": 131072, + "max_tokens": 131072, + "mode": "chat", + "output_cost_per_token": 6.5e-07, + "source": "https://melious.ai/hub/models", + "supports_function_calling": true, + "supports_response_schema": true, + "supports_tool_choice": true + }, + "melious/apertus-70b": { + "input_cost_per_token": 1.25e-06, + "litellm_provider": "melious", + "max_input_tokens": 65536, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 2e-06, + "source": "https://melious.ai/hub/models" + }, "pinstripes/ps/glm-4.5-air": { "max_tokens": 128000, "max_input_tokens": 128000, diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 65db63dc045..0046cc4d063 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -1492,6 +1492,24 @@ "interactions": true } }, + "melious": { + "display_name": "Melious (`melious`)", + "url": "https://docs.litellm.ai/docs/providers/melious", + "endpoints": { + "chat_completions": true, + "messages": true, + "responses": true, + "embeddings": false, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false, + "a2a": false, + "text_completion": false + } + }, "meta_llama": { "display_name": "Meta - Llama API (`meta_llama`)", "url": "https://docs.litellm.ai/docs/providers/meta_llama", diff --git a/tests/test_litellm/llms/melious/__init__.py b/tests/test_litellm/llms/melious/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_litellm/llms/melious/chat/__init__.py b/tests/test_litellm/llms/melious/chat/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_litellm/llms/melious/chat/test_melious_chat_transformation.py b/tests/test_litellm/llms/melious/chat/test_melious_chat_transformation.py new file mode 100644 index 00000000000..b75aa1fa815 --- /dev/null +++ b/tests/test_litellm/llms/melious/chat/test_melious_chat_transformation.py @@ -0,0 +1,130 @@ +from unittest.mock import patch + +import pytest + +import litellm +from litellm.llms.melious.chat.transformation import MeliousChatConfig +from litellm.utils import ProviderConfigManager + +MODEL = "melious/glm-5.1" + + +def test_provider_config_manager_returns_melious_chat_config(): + config = ProviderConfigManager.get_provider_chat_config( + model="glm-5.1", + provider=litellm.LlmProviders.MELIOUS, + ) + + assert isinstance(config, MeliousChatConfig) + assert config.custom_llm_provider == "melious" + + +@pytest.mark.parametrize( + "api_base", + [ + None, + "https://api.melious.ai", + "https://api.melious.ai/", + "https://api.melious.ai/v1", + "https://api.melious.ai/v1/", + "https://api.melious.ai/v1/chat/completions", + ], +) +def test_provider_info_normalizes_every_base_shape_to_v1(api_base): + """The OpenAI SDK path uses this value as its base_url, so it must be the /v1 root.""" + with patch("litellm.llms.melious.chat.transformation.get_secret_str", return_value=None): + resolved_api_base, _ = MeliousChatConfig()._get_openai_compatible_provider_info(api_base, "sk-mel-arg") + + assert resolved_api_base == "https://api.melious.ai/v1" + + +def test_provider_info_keeps_a_self_hosted_base_and_adds_v1(): + with patch("litellm.llms.melious.chat.transformation.get_secret_str", return_value=None): + resolved_api_base, _ = MeliousChatConfig()._get_openai_compatible_provider_info( + "https://melious.internal.example", "sk-mel-arg" + ) + + assert resolved_api_base == "https://melious.internal.example/v1" + + +def test_provider_info_prefers_the_argument_over_the_environment(): + secrets = {"MELIOUS_API_BASE": "https://env.melious.test/v1", "MELIOUS_API_KEY": "sk-mel-env"} + with patch( + "litellm.llms.melious.chat.transformation.get_secret_str", + side_effect=secrets.get, + ): + api_base, api_key = MeliousChatConfig()._get_openai_compatible_provider_info( + "https://arg.melious.test/v1", "sk-mel-arg" + ) + + assert api_base == "https://arg.melious.test/v1" + assert api_key == "sk-mel-arg" + + +def test_provider_info_reads_env_secrets(): + secrets = {"MELIOUS_API_BASE": "https://env.melious.test", "MELIOUS_API_KEY": "sk-mel-env"} + with patch( + "litellm.llms.melious.chat.transformation.get_secret_str", + side_effect=secrets.get, + ): + api_base, api_key = MeliousChatConfig()._get_openai_compatible_provider_info(None, None) + + assert api_base == "https://env.melious.test/v1" + assert api_key == "sk-mel-env" + + +def test_get_llm_provider_routes_the_melious_prefix(monkeypatch): + monkeypatch.setenv("MELIOUS_API_KEY", "sk-mel-env") + monkeypatch.delenv("MELIOUS_API_BASE", raising=False) + + model, custom_llm_provider, dynamic_api_key, api_base = litellm.get_llm_provider(model=MODEL) + + assert (model, custom_llm_provider) == ("glm-5.1", "melious") + assert dynamic_api_key == "sk-mel-env" + assert api_base == "https://api.melious.ai/v1" + + +def test_get_llm_provider_keeps_the_routing_flavor_suffix(monkeypatch): + """Melious selects a routing flavor via a `:` suffix, which must survive the split.""" + monkeypatch.setenv("MELIOUS_API_KEY", "sk-mel-env") + + model, custom_llm_provider, _, _ = litellm.get_llm_provider(model=f"{MODEL}:eco") + + assert (model, custom_llm_provider) == ("glm-5.1:eco", "melious") + + +def test_melious_is_registered_as_an_openai_compatible_chat_provider(): + from litellm.constants import LITELLM_CHAT_PROVIDERS, openai_compatible_providers + + assert "melious" in openai_compatible_providers + assert "melious" in LITELLM_CHAT_PROVIDERS + + +def test_supported_openai_params_inherit_the_openai_surface(): + params = litellm.get_supported_openai_params(model="glm-5.1", custom_llm_provider="melious") + + assert params is not None + for expected in ("stream", "temperature", "tools", "tool_choice", "response_format", "max_completion_tokens"): + assert expected in params + + +def test_get_optional_params_maps_through_the_melious_config(): + optional_params = litellm.utils.get_optional_params( + model="glm-5.1", + custom_llm_provider="melious", + max_completion_tokens=256, + temperature=0.2, + stream=True, + ) + + assert optional_params["max_completion_tokens"] == 256 + assert optional_params["temperature"] == 0.2 + assert optional_params["stream"] is True + + +def test_validate_environment_reports_the_melious_key(monkeypatch): + monkeypatch.delenv("MELIOUS_API_KEY", raising=False) + assert litellm.validate_environment(model=MODEL)["missing_keys"] == ["MELIOUS_API_KEY"] + + monkeypatch.setenv("MELIOUS_API_KEY", "sk-mel-env") + assert litellm.validate_environment(model=MODEL)["keys_in_environment"] is True diff --git a/tests/test_litellm/llms/melious/messages/__init__.py b/tests/test_litellm/llms/melious/messages/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_litellm/llms/melious/messages/test_melious_anthropic_messages_transformation.py b/tests/test_litellm/llms/melious/messages/test_melious_anthropic_messages_transformation.py new file mode 100644 index 00000000000..ec565615273 --- /dev/null +++ b/tests/test_litellm/llms/melious/messages/test_melious_anthropic_messages_transformation.py @@ -0,0 +1,148 @@ +import pytest + +import litellm +from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( + AnthropicMessagesConfig, +) +from litellm.llms.melious.messages.transformation import ( + MeliousAnthropicMessagesConfig, +) +from litellm.utils import ProviderConfigManager + +MESSAGES_URL = "https://api.melious.ai/v1/messages" + + +def complete_url(config, api_base): + return config.get_complete_url( + api_base=api_base, + api_key=None, + model="glm-5.1", + optional_params={}, + litellm_params={}, + ) + + +def test_provider_config_manager_returns_melious_messages_config(): + config = ProviderConfigManager.get_provider_anthropic_messages_config( + model="glm-5.1", + provider=litellm.LlmProviders.MELIOUS, + ) + + assert isinstance(config, MeliousAnthropicMessagesConfig) + assert config.custom_llm_provider == "melious" + + +def test_anthropic_provider_keeps_the_default_config(): + config = ProviderConfigManager.get_provider_anthropic_messages_config( + model="glm-5.1", + provider=litellm.LlmProviders.ANTHROPIC, + ) + + assert isinstance(config, AnthropicMessagesConfig) + assert not isinstance(config, MeliousAnthropicMessagesConfig) + + +def test_billing_metadata_is_stripped(): + """Melious rejects Anthropic's x-anthropic-billing-header system blocks.""" + assert MeliousAnthropicMessagesConfig().should_strip_billing_metadata() is True + + +@pytest.mark.parametrize( + "api_base", + [ + None, + "https://api.melious.ai", + "https://api.melious.ai/", + "https://api.melious.ai/v1", + "https://api.melious.ai/v1/", + "https://api.melious.ai/v1/messages", + "https://api.melious.ai/v1/chat/completions", + ], +) +def test_complete_url_normalizes_every_base_shape(api_base, monkeypatch): + monkeypatch.delenv("MELIOUS_API_BASE", raising=False) + + assert complete_url(MeliousAnthropicMessagesConfig(), api_base) == MESSAGES_URL + + +def test_complete_url_uses_a_self_hosted_base(monkeypatch): + monkeypatch.delenv("MELIOUS_API_BASE", raising=False) + + url = complete_url(MeliousAnthropicMessagesConfig(), "https://melious.internal.example") + + assert url == "https://melious.internal.example/v1/messages" + + +def test_complete_url_reads_the_api_base_env(monkeypatch): + monkeypatch.setenv("MELIOUS_API_BASE", "https://env.melious.test/v1") + + assert complete_url(MeliousAnthropicMessagesConfig(), None) == "https://env.melious.test/v1/messages" + + +def test_headers_carry_the_melious_key_from_the_environment(monkeypatch): + monkeypatch.setenv("MELIOUS_API_KEY", "sk-mel-env") + + headers, api_base = MeliousAnthropicMessagesConfig().validate_anthropic_messages_environment( + headers={}, + model="glm-5.1", + messages=[], + optional_params={}, + litellm_params={}, + api_key=None, + api_base=None, + ) + + assert headers["x-api-key"] == "sk-mel-env" + assert headers["anthropic-version"] == "2023-06-01" + assert headers["content-type"] == "application/json" + assert api_base is None + + +def test_headers_prefer_the_passed_key_over_the_environment(monkeypatch): + monkeypatch.setenv("MELIOUS_API_KEY", "sk-mel-env") + + headers, _ = MeliousAnthropicMessagesConfig().validate_anthropic_messages_environment( + headers={}, + model="glm-5.1", + messages=[], + optional_params={}, + litellm_params={}, + api_key="sk-mel-arg", + api_base=None, + ) + + assert headers["x-api-key"] == "sk-mel-arg" + + +def test_existing_auth_headers_are_left_alone(monkeypatch): + monkeypatch.setenv("MELIOUS_API_KEY", "sk-mel-env") + + headers, _ = MeliousAnthropicMessagesConfig().validate_anthropic_messages_environment( + headers={"authorization": "Bearer sk-mel-forwarded"}, + model="glm-5.1", + messages=[], + optional_params={}, + litellm_params={}, + api_key=None, + api_base=None, + ) + + assert "x-api-key" not in headers + assert headers["authorization"] == "Bearer sk-mel-forwarded" + + +def test_anthropic_beta_headers_are_still_injected(monkeypatch): + """The base config owns beta-header handling; the override must not drop it.""" + monkeypatch.setenv("MELIOUS_API_KEY", "sk-mel-env") + + headers, _ = MeliousAnthropicMessagesConfig().validate_anthropic_messages_environment( + headers={}, + model="glm-5.1", + messages=[], + optional_params={"output_format": {"type": "json_schema"}}, + litellm_params={}, + api_key=None, + api_base=None, + ) + + assert "structured-outputs-2025-11-13" in headers["anthropic-beta"] diff --git a/tests/test_litellm/llms/melious/test_common_utils.py b/tests/test_litellm/llms/melious/test_common_utils.py new file mode 100644 index 00000000000..c1647daa410 --- /dev/null +++ b/tests/test_litellm/llms/melious/test_common_utils.py @@ -0,0 +1,51 @@ +import pytest + +from litellm.llms.melious.common_utils import ( + MELIOUS_API_BASE, + MELIOUS_OPENAI_API_BASE, + anthropic_messages_url, + openai_api_base, +) + + +def test_defaults(): + assert MELIOUS_API_BASE == "https://api.melious.ai" + assert MELIOUS_OPENAI_API_BASE == "https://api.melious.ai/v1" + + +@pytest.mark.parametrize( + ("given", "expected"), + [ + ("https://api.melious.ai", "https://api.melious.ai/v1"), + ("https://api.melious.ai/", "https://api.melious.ai/v1"), + ("https://api.melious.ai/v1", "https://api.melious.ai/v1"), + ("https://api.melious.ai/v1/", "https://api.melious.ai/v1"), + ("https://api.melious.ai/v1/chat/completions", "https://api.melious.ai/v1"), + ("https://gateway.example/melious", "https://gateway.example/melious/v1"), + ], +) +def test_openai_api_base(given, expected): + assert openai_api_base(given) == expected + + +@pytest.mark.parametrize( + ("given", "expected"), + [ + ("https://api.melious.ai", "https://api.melious.ai/v1/messages"), + ("https://api.melious.ai/", "https://api.melious.ai/v1/messages"), + ("https://api.melious.ai/v1", "https://api.melious.ai/v1/messages"), + ("https://api.melious.ai/v1/", "https://api.melious.ai/v1/messages"), + ("https://api.melious.ai/v1/messages", "https://api.melious.ai/v1/messages"), + ("https://api.melious.ai/v1/messages/", "https://api.melious.ai/v1/messages"), + ("https://api.melious.ai/v1/chat/completions", "https://api.melious.ai/v1/messages"), + ("https://gateway.example/melious", "https://gateway.example/melious/v1/messages"), + ], +) +def test_anthropic_messages_url(given, expected): + assert anthropic_messages_url(given) == expected + + +def test_both_helpers_are_idempotent(): + assert openai_api_base(openai_api_base(MELIOUS_API_BASE)) == MELIOUS_OPENAI_API_BASE + once = anthropic_messages_url(MELIOUS_API_BASE) + assert anthropic_messages_url(once) == once diff --git a/tests/test_litellm/llms/melious/test_cost_calculator.py b/tests/test_litellm/llms/melious/test_cost_calculator.py new file mode 100644 index 00000000000..46cbd1d14fc --- /dev/null +++ b/tests/test_litellm/llms/melious/test_cost_calculator.py @@ -0,0 +1,51 @@ +import pytest + +import litellm +from litellm.llms.melious.cost_calculator import cost_per_token +from litellm.types.utils import Usage + +GLM_51_INPUT_COST = 1.3e-06 +GLM_51_OUTPUT_COST = 4.05e-06 + + +@pytest.fixture +def local_model_cost_map(monkeypatch): + original_model_cost = litellm.model_cost + monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") + litellm.model_cost = litellm.get_model_cost_map(url="") + litellm.get_model_info.cache_clear() + try: + yield + finally: + litellm.model_cost = original_model_cost + litellm.get_model_info.cache_clear() + + +def test_cost_per_token_uses_melious_model_pricing(local_model_cost_map): + usage = Usage(prompt_tokens=1000, completion_tokens=2000, total_tokens=3000) + + prompt_cost, completion_cost = cost_per_token(model="melious/glm-5.1", usage=usage) + + assert prompt_cost == pytest.approx(1000 * GLM_51_INPUT_COST) + assert completion_cost == pytest.approx(2000 * GLM_51_OUTPUT_COST) + + +def test_top_level_dispatcher_routes_melious_to_wrapper(local_model_cost_map): + from litellm.cost_calculator import cost_per_token as dispatch_cost_per_token + + prompt_cost, completion_cost = dispatch_cost_per_token( + model="melious/glm-5.1", + prompt_tokens=1000, + completion_tokens=1000, + custom_llm_provider="melious", + ) + + assert prompt_cost == pytest.approx(1000 * GLM_51_INPUT_COST) + assert completion_cost == pytest.approx(1000 * GLM_51_OUTPUT_COST) + + +def test_melious_models_are_registered_under_the_provider(local_model_cost_map): + litellm.add_known_models() + + assert "melious/glm-5.1" in litellm.models_by_provider["melious"] + assert litellm.get_model_info(model="melious/glm-5.1")["litellm_provider"] == "melious"