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
This commit is contained in:
s.agrawal 2026-08-15 00:31:44 +05:30
parent 423b791ee0
commit d82defad62
26 changed files with 1076 additions and 0 deletions

View file

@ -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) | ✅ | ✅ | ✅ | | ✅ | | | | | |

View file

@ -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]

View file

@ -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",

View file

@ -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",

View file

@ -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":

View file

@ -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
(

View file

@ -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":

View file

View file

View file

@ -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

View file

@ -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}"

View file

@ -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")

View file

@ -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))

View file

@ -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,

View file

@ -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

View file

@ -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 (

View file

@ -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,

View file

@ -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",

View file

@ -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 `:<flavor>` 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

View file

@ -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"]

View file

@ -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

View file

@ -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"