diff --git a/litellm/llms/morph/chat/transformation.py b/litellm/llms/morph/chat/transformation.py index 434397c8dfd..645aefc94c5 100644 --- a/litellm/llms/morph/chat/transformation.py +++ b/litellm/llms/morph/chat/transformation.py @@ -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 diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index b8e4331dfa2..70922fb543d 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -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, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index b8e4331dfa2..70922fb543d 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -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, diff --git a/tests/llm_translation/test_morph.py b/tests/llm_translation/test_morph.py index 752fb3b9083..9e7353afc15 100644 --- a/tests/llm_translation/test_morph.py +++ b/tests/llm_translation/test_morph.py @@ -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() diff --git a/tests/test_litellm/llms/morph/chat/test_morph_chat_transformation.py b/tests/test_litellm/llms/morph/chat/test_morph_chat_transformation.py new file mode 100644 index 00000000000..69cf31af933 --- /dev/null +++ b/tests/test_litellm/llms/morph/chat/test_morph_chat_transformation.py @@ -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"