This commit is contained in:
skeptrune 2026-09-23 14:47:43 +00:00 • committed by GitHub
commit 0934f41835
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 414 additions and 16 deletions

View file

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

View file

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

View file

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

View file

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

View file

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