mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-08 08:08:37 +00:00
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:
parent
423b791ee0
commit
d82defad62
26 changed files with 1076 additions and 0 deletions
|
|
@ -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) | ✅ | ✅ | ✅ | | ✅ | | | | | |
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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
|
||||
(
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
0
litellm/llms/melious/__init__.py
Normal file
0
litellm/llms/melious/__init__.py
Normal file
0
litellm/llms/melious/chat/__init__.py
Normal file
0
litellm/llms/melious/chat/__init__.py
Normal file
23
litellm/llms/melious/chat/transformation.py
Normal file
23
litellm/llms/melious/chat/transformation.py
Normal 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
|
||||
31
litellm/llms/melious/common_utils.py
Normal file
31
litellm/llms/melious/common_utils.py
Normal 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}"
|
||||
6
litellm/llms/melious/cost_calculator.py
Normal file
6
litellm/llms/melious/cost_calculator.py
Normal 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")
|
||||
0
litellm/llms/melious/messages/__init__.py
Normal file
0
litellm/llms/melious/messages/__init__.py
Normal file
63
litellm/llms/melious/messages/transformation.py
Normal file
63
litellm/llms/melious/messages/transformation.py
Normal 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))
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
0
tests/test_litellm/llms/melious/__init__.py
Normal file
0
tests/test_litellm/llms/melious/__init__.py
Normal file
0
tests/test_litellm/llms/melious/chat/__init__.py
Normal file
0
tests/test_litellm/llms/melious/chat/__init__.py
Normal 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
|
||||
0
tests/test_litellm/llms/melious/messages/__init__.py
Normal file
0
tests/test_litellm/llms/melious/messages/__init__.py
Normal 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"]
|
||||
51
tests/test_litellm/llms/melious/test_common_utils.py
Normal file
51
tests/test_litellm/llms/melious/test_common_utils.py
Normal 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
|
||||
51
tests/test_litellm/llms/melious/test_cost_calculator.py
Normal file
51
tests/test_litellm/llms/melious/test_cost_calculator.py
Normal 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"
|
||||
Loading…
Add table
Reference in a new issue