From 677b716c0824929de2ef28a21d768df33d7a7a08 Mon Sep 17 00:00:00 2001 From: skeptrune Date: Tue, 1 Sep 2026 17:54:01 -0700 Subject: [PATCH 1/3] feat: update Morph model support --- litellm/llms/morph/chat/transformation.py | 19 +++- ...odel_prices_and_context_window_backup.json | 100 ++++++++++++++++-- model_prices_and_context_window.json | 100 ++++++++++++++++-- tests/llm_translation/test_morph.py | 78 ++++++++++++-- 4 files changed, 278 insertions(+), 19 deletions(-) diff --git a/litellm/llms/morph/chat/transformation.py b/litellm/llms/morph/chat/transformation.py index 434397c8dfd..eceb2516220 100644 --- a/litellm/llms/morph/chat/transformation.py +++ b/litellm/llms/morph/chat/transformation.py @@ -25,14 +25,31 @@ class MorphChatConfig(OpenAILikeChatConfig): self, api_base: str | None, api_key: str | None ) -> tuple[str | None, str | None]: api_base = ( - api_base or get_secret_str("MORPH_API_BASE") or "https://api.morphllm.com/v1" # default api base + api_base + or get_secret_str("MORPH_API_BASE") + or "https://api.morphllm.com/v1" # default api base ) dynamic_api_key: Final = api_key or get_secret_str("MORPH_API_KEY") return api_base, dynamic_api_key def get_supported_openai_params(self, model: str) -> list: return [ + "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", ] diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 55618d9f772..8c8abea3f67 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -34650,11 +34650,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": 262144, "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, @@ -34664,17 +34665,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": 262144, "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 55618d9f772..8c8abea3f67 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -34650,11 +34650,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": 262144, "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, @@ -34664,17 +34665,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": 262144, "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 b91d1810d38..6bbc2638f0d 100644 --- a/tests/llm_translation/test_morph.py +++ b/tests/llm_translation/test_morph.py @@ -3,7 +3,6 @@ import os from unittest.mock import patch - import litellm from litellm import MorphChatConfig, get_llm_provider @@ -43,13 +42,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,7 +66,15 @@ 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", + ] ) @@ -76,9 +86,9 @@ def test_morph_model_info(): assert model_info["litellm_provider"] == "morph" assert model_info["mode"] == "chat" - assert model_info["max_tokens"] == 16000 - assert model_info["max_input_tokens"] == 16000 - assert model_info["max_output_tokens"] == 16000 + assert model_info["max_tokens"] == 262144 + 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 @@ -86,20 +96,76 @@ def test_morph_model_info(): 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") 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_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() From 81c3598ec832eeba4089c60c75f72ae83fd0675e Mon Sep 17 00:00:00 2001 From: skeptrune Date: Tue, 1 Sep 2026 17:56:06 -0700 Subject: [PATCH 2/3] fix: format Morph provider config --- litellm/llms/morph/chat/transformation.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/litellm/llms/morph/chat/transformation.py b/litellm/llms/morph/chat/transformation.py index eceb2516220..0d7fbc97281 100644 --- a/litellm/llms/morph/chat/transformation.py +++ b/litellm/llms/morph/chat/transformation.py @@ -25,9 +25,7 @@ class MorphChatConfig(OpenAILikeChatConfig): self, api_base: str | None, api_key: str | None ) -> tuple[str | None, str | None]: api_base = ( - api_base - or get_secret_str("MORPH_API_BASE") - or "https://api.morphllm.com/v1" # default api base + api_base or get_secret_str("MORPH_API_BASE") or "https://api.morphllm.com/v1" # default api base ) dynamic_api_key: Final = api_key or get_secret_str("MORPH_API_KEY") return api_base, dynamic_api_key From ccf2b455ddc2f4a46d04800ab02276a28fd1ea6f Mon Sep 17 00:00:00 2001 From: skeptrune Date: Sun, 13 Sep 2026 03:26:48 -0700 Subject: [PATCH 3/3] test: cover Morph tool param gating in unit test shard tests/llm_translation is outside the coverage-reporting unit shards, so add registry-gating tests under tests/test_litellm/llms/morph. Claude-Session: https://claude.ai/code/session_01LDxX4kpF7ajmSNbhzPsqNT --- .../chat/test_morph_chat_transformation.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/test_litellm/llms/morph/chat/test_morph_chat_transformation.py 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"