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
This commit is contained in:
moe-berri 2026-10-27 18:06:14 -07:00 • committed by GitHub
parent e5fe716d69
commit 696ff615d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 421 additions and 1 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

@ -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"},
)