From 696ff615d4def0d1e291383280d9849dcb1164ae Mon Sep 17 00:00:00 2001 From: moe-berri Date: Tue, 27 Oct 2026 18:06:14 -0700 Subject: [PATCH] feat(morph): support open models and cached input pricing (#48676) * feat(morph): support open-weight chat models and cached input pricing * chore(morph): preserve existing catalog number formatting * test(morph): include provider regressions in CI shard * fix(morph): satisfy catalog and test package checks * fix(morph): declare abliterated model output limit --- .github/workflows/test-unit.yml | 1 + litellm/llms/morph/chat/transformation.py | 22 ++- ...odel_prices_and_context_window_backup.json | 126 +++++++++++++++ model_prices_and_context_window.json | 126 +++++++++++++++ tests/unit/llms/morph/__init__.py | 1 + tests/unit/llms/morph/chat/__init__.py | 1 + .../llms/morph/chat/test_transformation.py | 145 ++++++++++++++++++ 7 files changed, 421 insertions(+), 1 deletion(-) create mode 100644 tests/unit/llms/morph/__init__.py create mode 100644 tests/unit/llms/morph/chat/__init__.py create mode 100644 tests/unit/llms/morph/chat/test_transformation.py diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index 4d47a882891..b4b7afb9a09 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -302,6 +302,7 @@ jobs: tests/unit/llms/modelscope tests/unit/llms/mongodb tests/unit/llms/moonshot + tests/unit/llms/morph tests/unit/llms/nadir tests/unit/llms/nebius tests/unit/llms/neosantara diff --git a/litellm/llms/morph/chat/transformation.py b/litellm/llms/morph/chat/transformation.py index d5cba501477..11c82ee64ff 100644 --- a/litellm/llms/morph/chat/transformation.py +++ b/litellm/llms/morph/chat/transformation.py @@ -37,9 +37,29 @@ class MorphChatConfig(OpenAILikeChatConfig): ) -> tuple[str | None, str | None]: return self._get_openai_compatible_provider_info(api_base, api_key) - def get_supported_openai_params(self, model: str) -> list: + def get_supported_openai_params(self, model: str) -> list[str]: + if model.removeprefix("morph/").startswith("morph-v3-"): + return ["messages", "model", "stream"] return [ "messages", "model", "stream", + "stream_options", + "temperature", + "top_p", + "frequency_penalty", + "presence_penalty", + "stop", + "seed", + "max_tokens", + "max_completion_tokens", + "logit_bias", + "logprobs", + "top_logprobs", + "tools", + "tool_choice", + "parallel_tool_calls", + "response_format", + "reasoning_effort", + "prompt_cache_key", ] diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 62914c65196..48ed74909c9 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -42059,6 +42059,132 @@ "supports_response_schema": true, "supports_tool_choice": true }, + "morph/morph-kimik3": { + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "input_cost_per_token": 2.5e-06, + "output_cost_per_token": 1.4e-05, + "cache_read_input_token_cost": 2.9e-07, + "litellm_provider": "morph", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_reasoning": true, + "supports_prompt_caching": true, + "source": "https://www.morphllm.com/pricing", + "supports_vision": true + }, + "morph/morph-kimik3-fast": { + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "input_cost_per_token": 6e-06, + "output_cost_per_token": 2.25e-05, + "cache_read_input_token_cost": 6e-07, + "litellm_provider": "morph", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_reasoning": true, + "supports_prompt_caching": true, + "source": "https://www.morphllm.com/pricing", + "supports_vision": true + }, + "morph/morph-glm53-744b": { + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "input_cost_per_token": 1.19e-06, + "output_cost_per_token": 6e-06, + "cache_read_input_token_cost": 1.955e-07, + "litellm_provider": "morph", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_reasoning": true, + "supports_prompt_caching": true, + "source": "https://www.morphllm.com/pricing" + }, + "morph/morph-glm53flash": { + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 7e-07, + "cache_read_input_token_cost": 4e-08, + "litellm_provider": "morph", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_reasoning": true, + "supports_prompt_caching": true, + "source": "https://www.morphllm.com/pricing", + "supports_vision": true + }, + "morph/morph-dsv41flash": { + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "input_cost_per_token": 1.2e-07, + "output_cost_per_token": 8e-07, + "cache_read_input_token_cost": 8e-09, + "litellm_provider": "morph", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_reasoning": true, + "supports_prompt_caching": true, + "source": "https://www.morphllm.com/pricing", + "supports_vision": true + }, + "morph/morph-dsv41flash-1017": { + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "input_cost_per_token": 1.2e-07, + "output_cost_per_token": 8e-07, + "cache_read_input_token_cost": 8e-09, + "litellm_provider": "morph", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_reasoning": true, + "supports_prompt_caching": true, + "source": "https://www.morphllm.com/pricing", + "supports_vision": true + }, + "morph/morph-dsv41flash-abliterated": { + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "input_cost_per_token": 3e-07, + "output_cost_per_token": 1.2e-06, + "cache_read_input_token_cost": 3e-08, + "litellm_provider": "morph", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_reasoning": true, + "supports_prompt_caching": true, + "source": "https://www.morphllm.com/pricing", + "supports_vision": true + }, + "morph/morph-dsv4flash": { + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "input_cost_per_token": 1.41953125e-07, + "output_cost_per_token": 3.99625e-07, + "cache_read_input_token_cost": 3.59375e-08, + "litellm_provider": "morph", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_reasoning": true, + "supports_prompt_caching": true, + "source": "https://www.morphllm.com/pricing" + }, "morph/morph-v3-fast": { "input_cost_per_token": 8e-07, "litellm_provider": "morph", diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 62914c65196..48ed74909c9 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -42059,6 +42059,132 @@ "supports_response_schema": true, "supports_tool_choice": true }, + "morph/morph-kimik3": { + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "input_cost_per_token": 2.5e-06, + "output_cost_per_token": 1.4e-05, + "cache_read_input_token_cost": 2.9e-07, + "litellm_provider": "morph", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_reasoning": true, + "supports_prompt_caching": true, + "source": "https://www.morphllm.com/pricing", + "supports_vision": true + }, + "morph/morph-kimik3-fast": { + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "input_cost_per_token": 6e-06, + "output_cost_per_token": 2.25e-05, + "cache_read_input_token_cost": 6e-07, + "litellm_provider": "morph", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_reasoning": true, + "supports_prompt_caching": true, + "source": "https://www.morphllm.com/pricing", + "supports_vision": true + }, + "morph/morph-glm53-744b": { + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "input_cost_per_token": 1.19e-06, + "output_cost_per_token": 6e-06, + "cache_read_input_token_cost": 1.955e-07, + "litellm_provider": "morph", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_reasoning": true, + "supports_prompt_caching": true, + "source": "https://www.morphllm.com/pricing" + }, + "morph/morph-glm53flash": { + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "input_cost_per_token": 2e-07, + "output_cost_per_token": 7e-07, + "cache_read_input_token_cost": 4e-08, + "litellm_provider": "morph", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_reasoning": true, + "supports_prompt_caching": true, + "source": "https://www.morphllm.com/pricing", + "supports_vision": true + }, + "morph/morph-dsv41flash": { + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "input_cost_per_token": 1.2e-07, + "output_cost_per_token": 8e-07, + "cache_read_input_token_cost": 8e-09, + "litellm_provider": "morph", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_reasoning": true, + "supports_prompt_caching": true, + "source": "https://www.morphllm.com/pricing", + "supports_vision": true + }, + "morph/morph-dsv41flash-1017": { + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "input_cost_per_token": 1.2e-07, + "output_cost_per_token": 8e-07, + "cache_read_input_token_cost": 8e-09, + "litellm_provider": "morph", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_reasoning": true, + "supports_prompt_caching": true, + "source": "https://www.morphllm.com/pricing", + "supports_vision": true + }, + "morph/morph-dsv41flash-abliterated": { + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "input_cost_per_token": 3e-07, + "output_cost_per_token": 1.2e-06, + "cache_read_input_token_cost": 3e-08, + "litellm_provider": "morph", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_reasoning": true, + "supports_prompt_caching": true, + "source": "https://www.morphllm.com/pricing", + "supports_vision": true + }, + "morph/morph-dsv4flash": { + "max_input_tokens": 1048576, + "max_output_tokens": 1048576, + "input_cost_per_token": 1.41953125e-07, + "output_cost_per_token": 3.99625e-07, + "cache_read_input_token_cost": 3.59375e-08, + "litellm_provider": "morph", + "mode": "chat", + "supports_function_calling": true, + "supports_parallel_function_calling": true, + "supports_response_schema": true, + "supports_reasoning": true, + "supports_prompt_caching": true, + "source": "https://www.morphllm.com/pricing" + }, "morph/morph-v3-fast": { "input_cost_per_token": 8e-07, "litellm_provider": "morph", diff --git a/tests/unit/llms/morph/__init__.py b/tests/unit/llms/morph/__init__.py new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/tests/unit/llms/morph/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/unit/llms/morph/chat/__init__.py b/tests/unit/llms/morph/chat/__init__.py new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/tests/unit/llms/morph/chat/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/unit/llms/morph/chat/test_transformation.py b/tests/unit/llms/morph/chat/test_transformation.py new file mode 100644 index 00000000000..577b518574d --- /dev/null +++ b/tests/unit/llms/morph/chat/test_transformation.py @@ -0,0 +1,145 @@ +import json +from typing import Final + +import pytest +import respx + +import litellm + + +@pytest.mark.parametrize("cached_tokens", [0, 80]) +def test_morph_forwards_chat_options_and_prices_cached_input(local_model_cost_map: None, cached_tokens: int) -> None: + messages: Final = [{"role": "user", "content": "Return a JSON greeting"}] + response_format: Final = {"type": "json_object"} + with respx.mock() as upstream: + route: Final = upstream.post("https://api.morphllm.com/v1/chat/completions").respond( + 200, + json={ + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 1, + "model": "morph-dsv41flash", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": '{"greeting":"hi"}'}, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": 100, + "completion_tokens": 5, + "total_tokens": 105, + "prompt_tokens_details": {"cached_tokens": cached_tokens}, + }, + }, + ) + response: Final = litellm.completion( + model="morph/morph-dsv41flash", + api_key="fake-test-key", + messages=messages, + response_format=response_format, + reasoning_effort="none", + max_completion_tokens=64, + prompt_cache_key="example-conversation", + extra_headers={"x-session-id": "example-conversation"}, + ) + request: Final = route.calls.last.request + assert request.headers["x-session-id"] == "example-conversation" + assert json.loads(request.content) == { + "model": "morph-dsv41flash", + "messages": messages, + "response_format": response_format, + "reasoning_effort": "none", + "max_tokens": 64, + "prompt_cache_key": "example-conversation", + } + assert response.usage.prompt_tokens_details.cached_tokens == cached_tokens + rates: Final = litellm.model_cost["morph/morph-dsv41flash"] + expected: Final = ( + (100 - cached_tokens) * rates["input_cost_per_token"] + + cached_tokens * rates["cache_read_input_token_cost"] + + 5 * rates["output_cost_per_token"] + ) + assert litellm.completion_cost(completion_response=response) == pytest.approx(expected) + + +def test_morph_preserves_tool_calls_and_stream_usage() -> None: + tool: Final = { + "type": "function", + "function": { + "name": "get_temperature", + "parameters": {"type": "object", "properties": {"city": {"type": "string"}}}, + }, + } + tool_call: Final = { + "index": 0, + "id": "call-test", + "type": "function", + "function": {"name": "get_temperature", "arguments": '{"city":"Paris"}'}, + } + chunks: Final = [ + { + "id": "chatcmpl-test", + "object": "chat.completion.chunk", + "created": 1, + "model": "morph-dsv41flash", + "choices": [{"index": 0, "delta": {"role": "assistant", "tool_calls": [tool_call]}, "finish_reason": None}], + }, + { + "id": "chatcmpl-test", + "object": "chat.completion.chunk", + "created": 1, + "model": "morph-dsv41flash", + "choices": [{"index": 0, "delta": {}, "finish_reason": "tool_calls"}], + }, + { + "id": "chatcmpl-test", + "object": "chat.completion.chunk", + "created": 1, + "model": "morph-dsv41flash", + "choices": [], + "usage": { + "prompt_tokens": 100, + "completion_tokens": 5, + "total_tokens": 105, + "prompt_tokens_details": {"cached_tokens": 80}, + }, + }, + ] + with respx.mock() as upstream: + route: Final = upstream.post("https://api.morphllm.com/v1/chat/completions").respond( + 200, + headers={"content-type": "text/event-stream"}, + text="".join("data: " + json.dumps(chunk) + "\n\n" for chunk in chunks) + "data: [DONE]\n\n", + ) + response: Final = litellm.stream_chunk_builder( + list( + litellm.completion( + model="morph/morph-dsv41flash", + api_key="fake-test-key", + messages=[{"role": "user", "content": "Get Paris temperature"}], + tools=[tool], + tool_choice="required", + stream=True, + stream_options={"include_usage": True}, + ) + ) + ) + body: Final = json.loads(route.calls.last.request.content) + assert body["tools"] == [tool] + assert body["tool_choice"] == "required" + assert body["stream_options"] == {"include_usage": True} + assert response.choices[0].message.tool_calls[0].function.arguments == '{"city":"Paris"}' + assert response.usage.prompt_tokens_details.cached_tokens == 80 + + +@pytest.mark.parametrize("model", ["morph-v3-fast", "morph-v3-large"]) +def test_morph_apply_keeps_rejecting_chat_only_options(model: str) -> None: + with pytest.raises(litellm.UnsupportedParamsError): + litellm.completion( + model="morph/" + model, + api_key="fake-test-key", + messages=[{"role": "user", "content": "Apply an edit"}], + response_format={"type": "json_object"}, + )