mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-28 01:32:17 +00:00
Merge ccf2b455dd into 2dccc0dc79
This commit is contained in:
commit
0934f41835
5 changed files with 414 additions and 16 deletions
|
|
@ -7,10 +7,13 @@ https://docs.morphllm.com/quickstart
|
|||
|
||||
from typing import Final
|
||||
|
||||
import litellm
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
|
||||
from ...openai_like.chat.transformation import OpenAILikeChatConfig
|
||||
|
||||
TOOL_CALLING_PARAMS: Final = frozenset({"tools", "tool_choice"})
|
||||
|
||||
|
||||
class MorphChatConfig(OpenAILikeChatConfig):
|
||||
"""
|
||||
|
|
@ -30,9 +33,35 @@ class MorphChatConfig(OpenAILikeChatConfig):
|
|||
dynamic_api_key: Final = api_key or get_secret_str("MORPH_API_KEY")
|
||||
return api_base, dynamic_api_key
|
||||
|
||||
@staticmethod
|
||||
def _registry_disables_function_calling(model: str) -> bool:
|
||||
# Morph ships models faster than the registry is updated, so only an
|
||||
# explicit `supports_function_calling: false` entry withholds tools.
|
||||
registry_key: Final = model if model.startswith("morph/") else f"morph/{model}"
|
||||
model_info: Final = litellm.model_cost.get(registry_key)
|
||||
return isinstance(model_info, dict) and model_info.get("supports_function_calling") is False
|
||||
|
||||
def get_supported_openai_params(self, model: str) -> list:
|
||||
return [
|
||||
supported_params: Final = [
|
||||
"extra_headers",
|
||||
"frequency_penalty",
|
||||
"logit_bias",
|
||||
"max_completion_tokens",
|
||||
"max_retries",
|
||||
"max_tokens",
|
||||
"messages",
|
||||
"model",
|
||||
"presence_penalty",
|
||||
"response_format",
|
||||
"seed",
|
||||
"stop",
|
||||
"stream",
|
||||
"stream_options",
|
||||
"temperature",
|
||||
"tool_choice",
|
||||
"tools",
|
||||
"top_p",
|
||||
]
|
||||
if self._registry_disables_function_calling(model):
|
||||
return [param for param in supported_params if param not in TOOL_CALLING_PARAMS]
|
||||
return supported_params
|
||||
|
|
|
|||
|
|
@ -37095,11 +37095,12 @@
|
|||
"morph/morph-v3-fast": {
|
||||
"input_cost_per_token": 8e-07,
|
||||
"litellm_provider": "morph",
|
||||
"max_input_tokens": 16000,
|
||||
"max_output_tokens": 16000,
|
||||
"max_tokens": 16000,
|
||||
"max_input_tokens": 262144,
|
||||
"max_output_tokens": 131072,
|
||||
"max_tokens": 131072,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.2e-06,
|
||||
"source": "https://www.morphllm.com/api/models/json",
|
||||
"supports_function_calling": false,
|
||||
"supports_parallel_function_calling": false,
|
||||
"supports_system_messages": true,
|
||||
|
|
@ -37109,17 +37110,104 @@
|
|||
"morph/morph-v3-large": {
|
||||
"input_cost_per_token": 9e-07,
|
||||
"litellm_provider": "morph",
|
||||
"max_input_tokens": 16000,
|
||||
"max_output_tokens": 16000,
|
||||
"max_tokens": 16000,
|
||||
"max_input_tokens": 262144,
|
||||
"max_output_tokens": 131072,
|
||||
"max_tokens": 131072,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.9e-06,
|
||||
"source": "https://www.morphllm.com/api/models/json",
|
||||
"supports_function_calling": false,
|
||||
"supports_parallel_function_calling": false,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": false,
|
||||
"supports_vision": false
|
||||
},
|
||||
"morph/morph-dsv4flash": {
|
||||
"cache_read_input_token_cost": 2.5e-08,
|
||||
"input_cost_per_token": 9.875e-08,
|
||||
"litellm_provider": "morph",
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 1048576,
|
||||
"max_tokens": 1048576,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 2.78e-07,
|
||||
"source": "https://www.morphllm.com/api/models/json",
|
||||
"supports_function_calling": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_vision": false
|
||||
},
|
||||
"morph/morph-glm53-744b": {
|
||||
"cache_read_input_token_cost": 2.6e-07,
|
||||
"input_cost_per_token": 1.25e-06,
|
||||
"litellm_provider": "morph",
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 1048576,
|
||||
"max_tokens": 1048576,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 4.4e-06,
|
||||
"source": "https://www.morphllm.com/api/models/json",
|
||||
"supports_function_calling": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_vision": false
|
||||
},
|
||||
"morph/morph-glm53flash": {
|
||||
"cache_read_input_token_cost": 1e-08,
|
||||
"input_cost_per_token": 1.5e-07,
|
||||
"litellm_provider": "morph",
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 1048576,
|
||||
"max_tokens": 1048576,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 4.2e-07,
|
||||
"source": "https://www.morphllm.com/api/models/json",
|
||||
"supports_function_calling": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_video_input": true,
|
||||
"supports_vision": true
|
||||
},
|
||||
"morph/morph-kimik3": {
|
||||
"cache_read_input_token_cost": 2.9e-07,
|
||||
"input_cost_per_token": 2.8e-06,
|
||||
"litellm_provider": "morph",
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 1048576,
|
||||
"max_tokens": 1048576,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.4e-05,
|
||||
"source": "https://www.morphllm.com/api/models/json",
|
||||
"supports_function_calling": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_vision": true
|
||||
},
|
||||
"morph/morph-kimik3-fast": {
|
||||
"cache_read_input_token_cost": 6e-07,
|
||||
"input_cost_per_token": 6e-06,
|
||||
"litellm_provider": "morph",
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 1048576,
|
||||
"max_tokens": 1048576,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 2.25e-05,
|
||||
"source": "https://www.morphllm.com/api/models/json",
|
||||
"supports_function_calling": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_vision": true
|
||||
},
|
||||
"multimodalembedding": {
|
||||
"input_cost_per_character": 2e-07,
|
||||
"input_cost_per_image": 0.0001,
|
||||
|
|
|
|||
|
|
@ -37095,11 +37095,12 @@
|
|||
"morph/morph-v3-fast": {
|
||||
"input_cost_per_token": 8e-07,
|
||||
"litellm_provider": "morph",
|
||||
"max_input_tokens": 16000,
|
||||
"max_output_tokens": 16000,
|
||||
"max_tokens": 16000,
|
||||
"max_input_tokens": 262144,
|
||||
"max_output_tokens": 131072,
|
||||
"max_tokens": 131072,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.2e-06,
|
||||
"source": "https://www.morphllm.com/api/models/json",
|
||||
"supports_function_calling": false,
|
||||
"supports_parallel_function_calling": false,
|
||||
"supports_system_messages": true,
|
||||
|
|
@ -37109,17 +37110,104 @@
|
|||
"morph/morph-v3-large": {
|
||||
"input_cost_per_token": 9e-07,
|
||||
"litellm_provider": "morph",
|
||||
"max_input_tokens": 16000,
|
||||
"max_output_tokens": 16000,
|
||||
"max_tokens": 16000,
|
||||
"max_input_tokens": 262144,
|
||||
"max_output_tokens": 131072,
|
||||
"max_tokens": 131072,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.9e-06,
|
||||
"source": "https://www.morphllm.com/api/models/json",
|
||||
"supports_function_calling": false,
|
||||
"supports_parallel_function_calling": false,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": false,
|
||||
"supports_vision": false
|
||||
},
|
||||
"morph/morph-dsv4flash": {
|
||||
"cache_read_input_token_cost": 2.5e-08,
|
||||
"input_cost_per_token": 9.875e-08,
|
||||
"litellm_provider": "morph",
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 1048576,
|
||||
"max_tokens": 1048576,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 2.78e-07,
|
||||
"source": "https://www.morphllm.com/api/models/json",
|
||||
"supports_function_calling": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_vision": false
|
||||
},
|
||||
"morph/morph-glm53-744b": {
|
||||
"cache_read_input_token_cost": 2.6e-07,
|
||||
"input_cost_per_token": 1.25e-06,
|
||||
"litellm_provider": "morph",
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 1048576,
|
||||
"max_tokens": 1048576,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 4.4e-06,
|
||||
"source": "https://www.morphllm.com/api/models/json",
|
||||
"supports_function_calling": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_vision": false
|
||||
},
|
||||
"morph/morph-glm53flash": {
|
||||
"cache_read_input_token_cost": 1e-08,
|
||||
"input_cost_per_token": 1.5e-07,
|
||||
"litellm_provider": "morph",
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 1048576,
|
||||
"max_tokens": 1048576,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 4.2e-07,
|
||||
"source": "https://www.morphllm.com/api/models/json",
|
||||
"supports_function_calling": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_video_input": true,
|
||||
"supports_vision": true
|
||||
},
|
||||
"morph/morph-kimik3": {
|
||||
"cache_read_input_token_cost": 2.9e-07,
|
||||
"input_cost_per_token": 2.8e-06,
|
||||
"litellm_provider": "morph",
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 1048576,
|
||||
"max_tokens": 1048576,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 1.4e-05,
|
||||
"source": "https://www.morphllm.com/api/models/json",
|
||||
"supports_function_calling": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_vision": true
|
||||
},
|
||||
"morph/morph-kimik3-fast": {
|
||||
"cache_read_input_token_cost": 6e-07,
|
||||
"input_cost_per_token": 6e-06,
|
||||
"litellm_provider": "morph",
|
||||
"max_input_tokens": 1048576,
|
||||
"max_output_tokens": 1048576,
|
||||
"max_tokens": 1048576,
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 2.25e-05,
|
||||
"source": "https://www.morphllm.com/api/models/json",
|
||||
"supports_function_calling": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_vision": true
|
||||
},
|
||||
"multimodalembedding": {
|
||||
"input_cost_per_character": 2e-07,
|
||||
"input_cost_per_image": 0.0001,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm import MorphChatConfig, get_llm_provider
|
||||
|
|
@ -43,13 +44,16 @@ def test_morph_get_llm_provider():
|
|||
_, custom_llm_provider, _, _ = get_llm_provider("morph/morph-v3-fast")
|
||||
assert custom_llm_provider == "morph"
|
||||
|
||||
_, custom_llm_provider, _, _ = get_llm_provider("morph/morph-kimik3")
|
||||
assert custom_llm_provider == "morph"
|
||||
|
||||
|
||||
def test_morph_in_provider_lists():
|
||||
"""Test that morph is included in all necessary provider lists."""
|
||||
import litellm
|
||||
from litellm.constants import (
|
||||
openai_compatible_providers,
|
||||
openai_compatible_endpoints,
|
||||
openai_compatible_providers,
|
||||
)
|
||||
|
||||
# Check morph is in openai_compatible_providers
|
||||
|
|
@ -64,24 +68,137 @@ def test_morph_in_provider_lists():
|
|||
# Check models are in model_list after initialization
|
||||
assert all(
|
||||
model in litellm.model_list
|
||||
for model in ["morph/morph-v3-large", "morph/morph-v3-fast"]
|
||||
for model in [
|
||||
"morph/morph-dsv4flash",
|
||||
"morph/morph-glm53-744b",
|
||||
"morph/morph-glm53flash",
|
||||
"morph/morph-kimik3",
|
||||
"morph/morph-kimik3-fast",
|
||||
"morph/morph-v3-large",
|
||||
"morph/morph-v3-fast",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_morph_model_info():
|
||||
"""Test that morph models have correct configuration."""
|
||||
import litellm
|
||||
|
||||
model_info = litellm.get_model_info("morph/morph-v3-large")
|
||||
|
||||
assert model_info["litellm_provider"] == "morph"
|
||||
assert model_info["mode"] == "chat"
|
||||
assert model_info["max_tokens"] == 131072
|
||||
assert model_info["max_input_tokens"] == 262144
|
||||
assert model_info["max_output_tokens"] == 131072
|
||||
assert model_info["input_cost_per_token"] == 9e-07 # $0.9/1M tokens
|
||||
assert model_info["output_cost_per_token"] == 1.9e-06 # $1.9/1M tokens
|
||||
assert model_info["supports_function_calling"] is False
|
||||
assert model_info["supports_vision"] is False
|
||||
assert model_info["supports_system_messages"] is True
|
||||
|
||||
|
||||
def test_morph_open_model_info():
|
||||
model_info = litellm.get_model_info("morph/morph-glm53flash")
|
||||
|
||||
assert model_info["litellm_provider"] == "morph"
|
||||
assert model_info["mode"] == "chat"
|
||||
assert model_info["max_input_tokens"] == 1048576
|
||||
assert model_info["input_cost_per_token"] == 1.5e-07
|
||||
assert model_info["cache_read_input_token_cost"] == 1e-08
|
||||
assert model_info["output_cost_per_token"] == 4.2e-07
|
||||
assert model_info["supports_function_calling"] is True
|
||||
assert model_info["supports_prompt_caching"] is True
|
||||
assert model_info["supports_response_schema"] is True
|
||||
assert model_info["supports_vision"] is True
|
||||
|
||||
|
||||
def test_morph_supported_params():
|
||||
"""Test that MorphChatConfig returns correct supported parameters."""
|
||||
config = MorphChatConfig()
|
||||
supported_params = config.get_supported_openai_params("morph/morph-v3-large")
|
||||
supported_params = config.get_supported_openai_params("morph/morph-glm53flash")
|
||||
|
||||
expected_params = [
|
||||
"frequency_penalty",
|
||||
"max_tokens",
|
||||
"messages",
|
||||
"model",
|
||||
"presence_penalty",
|
||||
"response_format",
|
||||
"seed",
|
||||
"stop",
|
||||
"stream",
|
||||
"temperature",
|
||||
"tool_choice",
|
||||
"tools",
|
||||
"top_p",
|
||||
]
|
||||
|
||||
assert all(param in supported_params for param in expected_params)
|
||||
|
||||
|
||||
def test_morph_withholds_tool_params_for_models_without_function_calling():
|
||||
config = MorphChatConfig()
|
||||
|
||||
for model in ["morph/morph-v3-large", "morph-v3-fast"]:
|
||||
supported_params = config.get_supported_openai_params(model)
|
||||
|
||||
assert "tools" not in supported_params
|
||||
assert "tool_choice" not in supported_params
|
||||
assert "temperature" in supported_params
|
||||
|
||||
|
||||
def test_morph_passes_tool_params_for_models_missing_from_registry():
|
||||
config = MorphChatConfig()
|
||||
supported_params = config.get_supported_openai_params("morph/morph-unreleased-model")
|
||||
|
||||
assert "tools" in supported_params
|
||||
assert "tool_choice" in supported_params
|
||||
|
||||
|
||||
def test_morph_rejects_tools_for_models_without_function_calling():
|
||||
tools = [{"type": "function", "function": {"name": "noop", "parameters": {"type": "object", "properties": {}}}}]
|
||||
|
||||
with pytest.raises(litellm.UnsupportedParamsError):
|
||||
litellm.get_optional_params(
|
||||
model="morph-v3-large",
|
||||
custom_llm_provider="morph",
|
||||
tools=tools,
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
|
||||
def test_morph_maps_tool_and_response_format_params():
|
||||
config = MorphChatConfig()
|
||||
tools = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
}
|
||||
]
|
||||
response_format = {"type": "json_object"}
|
||||
|
||||
mapped = config.map_openai_params(
|
||||
non_default_params={
|
||||
"max_completion_tokens": 64,
|
||||
"response_format": response_format,
|
||||
"tool_choice": "required",
|
||||
"tools": tools,
|
||||
},
|
||||
optional_params={},
|
||||
model="morph/morph-glm53flash",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert mapped["max_tokens"] == 64
|
||||
assert mapped["response_format"] == response_format
|
||||
assert mapped["tool_choice"] == "required"
|
||||
assert mapped["tools"] == tools
|
||||
|
||||
|
||||
def test_morph_custom_llm_provider():
|
||||
"""Test that morph models are correctly identified."""
|
||||
config = MorphChatConfig()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.exceptions import UnsupportedParamsError
|
||||
from litellm.llms.morph.chat.transformation import MorphChatConfig
|
||||
|
||||
NO_TOOLS_MODEL = "morph-v3-large"
|
||||
TOOL_CALLING_MODEL = "morph-glm53flash"
|
||||
UNMAPPED_MODEL = "morph-unreleased-model"
|
||||
TOOLS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {"name": "noop", "parameters": {"type": "object", "properties": {}}},
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def local_model_cost_map(monkeypatch):
|
||||
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
|
||||
monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url=""))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", [NO_TOOLS_MODEL, f"morph/{NO_TOOLS_MODEL}", "morph-v3-fast"])
|
||||
def test_tool_params_are_withheld_when_registry_disables_function_calling(model):
|
||||
supported_params = MorphChatConfig().get_supported_openai_params(model)
|
||||
|
||||
assert "tools" not in supported_params
|
||||
assert "tool_choice" not in supported_params
|
||||
assert "response_format" in supported_params
|
||||
assert "temperature" in supported_params
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", [TOOL_CALLING_MODEL, f"morph/{TOOL_CALLING_MODEL}", UNMAPPED_MODEL])
|
||||
def test_tool_params_pass_through_unless_registry_disables_function_calling(model):
|
||||
supported_params = MorphChatConfig().get_supported_openai_params(model)
|
||||
|
||||
assert "tools" in supported_params
|
||||
assert "tool_choice" in supported_params
|
||||
|
||||
|
||||
def test_tools_for_model_without_function_calling_raise_unsupported_params():
|
||||
with pytest.raises(UnsupportedParamsError):
|
||||
litellm.get_optional_params(
|
||||
model=NO_TOOLS_MODEL,
|
||||
custom_llm_provider="morph",
|
||||
tools=TOOLS,
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
|
||||
def test_tools_for_model_without_function_calling_are_dropped_with_drop_params():
|
||||
optional_params = litellm.get_optional_params(
|
||||
model=NO_TOOLS_MODEL,
|
||||
custom_llm_provider="morph",
|
||||
tools=TOOLS,
|
||||
tool_choice="auto",
|
||||
temperature=0.2,
|
||||
drop_params=True,
|
||||
)
|
||||
|
||||
assert "tools" not in optional_params
|
||||
assert "tool_choice" not in optional_params
|
||||
assert optional_params["temperature"] == 0.2
|
||||
|
||||
|
||||
def test_tools_for_tool_calling_model_are_forwarded():
|
||||
optional_params = litellm.get_optional_params(
|
||||
model=TOOL_CALLING_MODEL,
|
||||
custom_llm_provider="morph",
|
||||
tools=TOOLS,
|
||||
tool_choice="required",
|
||||
)
|
||||
|
||||
assert optional_params["tools"] == TOOLS
|
||||
assert optional_params["tool_choice"] == "required"
|
||||
Loading…
Add table
Reference in a new issue