mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
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:
parent
e5fe716d69
commit
696ff615d4
7 changed files with 421 additions and 1 deletions
1
.github/workflows/test-unit.yml
vendored
1
.github/workflows/test-unit.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
1
tests/unit/llms/morph/__init__.py
Normal file
1
tests/unit/llms/morph/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
1
tests/unit/llms/morph/chat/__init__.py
Normal file
1
tests/unit/llms/morph/chat/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
145
tests/unit/llms/morph/chat/test_transformation.py
Normal file
145
tests/unit/llms/morph/chat/test_transformation.py
Normal 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"},
|
||||
)
|
||||
Loading…
Add table
Reference in a new issue