Skip to content

Commit 7fa55f6

Browse files
tests
1 parent 2306411 commit 7fa55f6

File tree

6 files changed

+625
-21
lines changed

6 files changed

+625
-21
lines changed

litellm/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,8 @@ def add_known_models():
858858
from .llms.github.chat.transformation import GithubChatConfig
859859
from .llms.empower.chat.transformation import EmpowerChatConfig
860860
from .llms.morph.chat.transformation import MorphChatConfig
861+
from .llms.morph.embedding.transformation import MorphEmbeddingConfig
862+
from .llms.morph.rerank.transformation import MorphRerankConfig
861863
from .llms.huggingface.chat.transformation import HuggingFaceChatConfig
862864
from .llms.huggingface.embedding.transformation import HuggingFaceEmbeddingConfig
863865
from .llms.oobabooga.chat.transformation import OobaboogaConfig
@@ -1044,7 +1046,6 @@ def add_known_models():
10441046
)
10451047
from .llms.friendliai.chat.transformation import FriendliaiChatConfig
10461048
from .llms.jina_ai.embedding.transformation import JinaAIEmbeddingConfig
1047-
from .llms.morph.embedding.transformation import MorphEmbeddingConfig
10481049
from .llms.xai.chat.transformation import XAIChatConfig
10491050
from .llms.xai.common_utils import XAIModelInfo
10501051
from .llms.volcengine import VolcEngineConfig

litellm/llms/morph/embedding/transformation.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,50 @@
22
Translate from OpenAI's `/v1/embeddings` to Morph's `/v1/embeddings`
33
"""
44

5-
from ...openai_like.embedding.handler import OpenAILikeEmbeddingHandler
5+
from typing import Optional, Tuple
66

7+
from litellm.llms.base_llm.embedding.transformation import BaseEmbeddingConfig
8+
from litellm.secret_managers.main import get_secret_str
79

8-
class MorphEmbeddingConfig(OpenAILikeEmbeddingHandler):
9-
pass
10+
11+
class MorphEmbeddingConfig(BaseEmbeddingConfig):
12+
"""
13+
Reference: https://docs.morphllm.com/api-reference/endpoint/embeddings
14+
15+
Morph provides an OpenAI-compatible embeddings API.
16+
"""
17+
18+
def __init__(self) -> None:
19+
pass
20+
21+
def _get_openai_compatible_provider_info(
22+
self, api_base: Optional[str], api_key: Optional[str]
23+
) -> Tuple[Optional[str], Optional[str]]:
24+
# Morph is OpenAI compatible, set to custom_openai and use Morph's endpoint
25+
api_base = (
26+
api_base
27+
or get_secret_str("MORPH_API_BASE")
28+
or "https://api.morphllm.com/v1"
29+
)
30+
dynamic_api_key = api_key or get_secret_str("MORPH_API_KEY")
31+
return api_base, dynamic_api_key
32+
33+
def validate_environment(
34+
self,
35+
headers: dict,
36+
model: str,
37+
optional_params: dict,
38+
api_key: Optional[str] = None,
39+
api_base: Optional[str] = None,
40+
) -> dict:
41+
"""Validate that the necessary API key is available."""
42+
if api_key is None:
43+
api_key = get_secret_str("MORPH_API_KEY")
44+
45+
if api_key is None:
46+
raise ValueError("Morph API key is required. Please set 'MORPH_API_KEY' environment variable.")
47+
48+
headers["Authorization"] = f"Bearer {api_key}"
49+
headers["Content-Type"] = "application/json"
50+
51+
return headers

litellm/llms/morph/rerank/transformation.py

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22

33
import httpx
44

5-
import litellm
65
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
76
from litellm.llms.base_llm.chat.transformation import BaseLLMException
87
from litellm.llms.base_llm.rerank.transformation import BaseRerankConfig
98
from litellm.secret_managers.main import get_secret_str
10-
from litellm.types.rerank import OptionalRerankParams, RerankRequest
9+
from litellm.types.rerank import OptionalRerankParams
1110
from litellm.types.utils import RerankResponse
1211

1312

@@ -42,14 +41,17 @@ def get_supported_cohere_rerank_params(self, model: str) -> list:
4241

4342
def map_cohere_rerank_params(
4443
self,
45-
non_default_params: Optional[dict],
44+
non_default_params: dict,
4645
model: str,
4746
drop_params: bool,
4847
query: str,
4948
documents: List[Union[str, Dict[str, Any]]],
5049
custom_llm_provider: Optional[str] = None,
5150
top_n: Optional[int] = None,
52-
return_documents: Optional[bool] = True,
51+
rank_fields: Optional[List[str]] = None,
52+
return_documents: Optional[bool] = None,
53+
max_chunks_per_doc: Optional[int] = None,
54+
max_tokens_per_doc: Optional[int] = None,
5355
embedding_ids: Optional[List[str]] = None,
5456
) -> OptionalRerankParams:
5557
"""
@@ -58,18 +60,25 @@ def map_cohere_rerank_params(
5860
Returns all supported params
5961
"""
6062
# Start with basic parameters
61-
params = OptionalRerankParams(
62-
query=query,
63-
documents=documents,
64-
top_n=top_n,
65-
return_documents=return_documents,
66-
)
63+
params: OptionalRerankParams = {
64+
"query": query,
65+
"documents": documents,
66+
}
6767

68+
# Add optional parameters
69+
if top_n is not None:
70+
params["top_n"] = top_n
71+
if return_documents is not None:
72+
params["return_documents"] = return_documents
73+
6874
# Add Morph-specific parameter if available
69-
if embedding_ids:
70-
params["embedding_ids"] = embedding_ids
75+
if embedding_ids is not None:
76+
# We can't add this directly to OptionalRerankParams because it's not in the TypedDict
77+
# but we need to pass it to the API, so we'll add it as a custom parameter
78+
params = dict(params) # type: ignore
79+
params["embedding_ids"] = embedding_ids # type: ignore
7180

72-
return params
81+
return params # type: ignore
7382

7483
def validate_environment(
7584
self,
@@ -106,7 +115,10 @@ def transform_rerank_request(
106115
raise ValueError("query is required for Morph rerank")
107116

108117
# Either documents or embedding_ids must be provided
109-
if "documents" not in optional_rerank_params and "embedding_ids" not in optional_rerank_params:
118+
documents_provided = "documents" in optional_rerank_params
119+
embedding_ids_provided = "embedding_ids" in optional_rerank_params # type: ignore
120+
121+
if not documents_provided and not embedding_ids_provided:
110122
raise ValueError("Either documents or embedding_ids is required for Morph rerank")
111123

112124
# Create request with the model name stripped of any prefix
@@ -116,10 +128,10 @@ def transform_rerank_request(
116128
}
117129

118130
# Add either documents or embedding_ids
119-
if "documents" in optional_rerank_params:
131+
if documents_provided:
120132
request_data["documents"] = optional_rerank_params["documents"]
121-
if "embedding_ids" in optional_rerank_params:
122-
request_data["embedding_ids"] = optional_rerank_params["embedding_ids"]
133+
if embedding_ids_provided:
134+
request_data["embedding_ids"] = optional_rerank_params.get("embedding_ids") # type: ignore
123135

124136
# Add optional parameters
125137
if "top_n" in optional_rerank_params:
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
"""
2+
Unit tests for Morph configuration.
3+
4+
These tests validate the MorphChatConfig class which extends OpenAILikeChatConfig.
5+
Morph is an OpenAI-compatible provider with a few customizations.
6+
"""
7+
8+
import os
9+
import sys
10+
from typing import Dict, List, Optional
11+
from unittest.mock import patch
12+
13+
import pytest
14+
15+
sys.path.insert(
16+
0, os.path.abspath("../../../../..")
17+
) # Adds the parent directory to the system path
18+
19+
from litellm.llms.morph.chat.transformation import MorphChatConfig
20+
21+
22+
class TestMorphChatConfig:
23+
"""Test class for MorphChatConfig functionality"""
24+
25+
def test_validate_environment(self):
26+
"""Test that validate_environment adds correct headers"""
27+
config = MorphChatConfig()
28+
headers = {}
29+
api_key = "fake-morph-key"
30+
31+
result = config.validate_environment(
32+
headers=headers,
33+
model="morph/apply-v1",
34+
messages=[{"role": "user", "content": "Hello"}],
35+
optional_params={},
36+
litellm_params={},
37+
api_key=api_key,
38+
api_base="https://api.morphllm.com/v1",
39+
)
40+
41+
# Verify headers
42+
assert result["Authorization"] == f"Bearer {api_key}"
43+
assert result["Content-Type"] == "application/json"
44+
45+
def test_get_openai_compatible_provider_info(self):
46+
"""Test the _get_openai_compatible_provider_info method"""
47+
config = MorphChatConfig()
48+
api_key = "fake-morph-key"
49+
50+
result = config._get_openai_compatible_provider_info(
51+
api_base=None,
52+
api_key=api_key,
53+
)
54+
55+
# Verify correct API base is returned
56+
assert result[0] == "https://api.morphllm.com/v1"
57+
assert result[1] == api_key
58+
59+
def test_missing_api_key(self):
60+
"""Test error handling when API key is missing"""
61+
config = MorphChatConfig()
62+
63+
with pytest.raises(ValueError) as excinfo:
64+
config.validate_environment(
65+
headers={},
66+
model="morph/apply-v1",
67+
messages=[{"role": "user", "content": "Hello"}],
68+
optional_params={},
69+
litellm_params={},
70+
api_key=None,
71+
api_base="https://api.morphllm.com/v1",
72+
)
73+
74+
assert "Morph API key is required" in str(excinfo.value)
75+
76+
def test_inheritance(self):
77+
"""Test proper inheritance from OpenAILikeChatConfig"""
78+
config = MorphChatConfig()
79+
80+
from litellm.llms.openai_like.chat.transformation import OpenAILikeChatConfig
81+
82+
assert isinstance(config, OpenAILikeChatConfig)
83+
assert hasattr(config, "_get_openai_compatible_provider_info")
84+
85+
def test_morph_completion_mock(self, respx_mock):
86+
"""
87+
Mock test for Morph completion using the model format from docs.
88+
This test mocks the actual HTTP request to test the integration properly.
89+
"""
90+
import respx
91+
from litellm import completion
92+
93+
# Set up environment variables for the test
94+
api_key = "fake-morph-key"
95+
api_base = "https://api.morphllm.com/v1"
96+
model = "morph/apply-v1"
97+
98+
# Mock the HTTP request to the Morph API
99+
respx_mock.post(f"{api_base}/chat/completions").respond(
100+
json={
101+
"id": "chatcmpl-123",
102+
"object": "chat.completion",
103+
"created": 1677652288,
104+
"model": "apply-v1",
105+
"choices": [
106+
{
107+
"index": 0,
108+
"message": {
109+
"role": "assistant",
110+
"content": "```python\nprint(\"Hi from LiteLLM!\")\n```\n\nThis simple Python code prints a greeting message from LiteLLM.",
111+
},
112+
"finish_reason": "stop",
113+
}
114+
],
115+
"usage": {"prompt_tokens": 9, "completion_tokens": 12, "total_tokens": 21},
116+
},
117+
status_code=200
118+
)
119+
120+
# Make the actual API call through LiteLLM
121+
response = completion(
122+
model=model,
123+
messages=[{"role": "user", "content": "write code for saying hi from LiteLLM"}],
124+
api_key=api_key,
125+
api_base=api_base
126+
)
127+
128+
# Verify response structure
129+
assert response is not None
130+
assert hasattr(response, "choices")
131+
assert len(response.choices) > 0
132+
assert hasattr(response.choices[0], "message")
133+
assert hasattr(response.choices[0].message, "content")
134+
assert response.choices[0].message.content is not None
135+
136+
# Check for specific content in the response
137+
assert "```python" in response.choices[0].message.content
138+
assert "Hi from LiteLLM" in response.choices[0].message.content
139+
140+
def test_morph_apply_code_updates(self, respx_mock):
141+
"""
142+
Test Morph's Apply Code Updates functionality which uses special tags
143+
for code and updates as per https://docs.morphllm.com/api-reference/endpoint/apply
144+
"""
145+
import respx
146+
from litellm import completion
147+
148+
# Set up environment variables for the test
149+
api_key = "fake-morph-key"
150+
api_base = "https://api.morphllm.com/v1"
151+
model = "morph/apply-v1"
152+
153+
# Original code and update with Morph's special tags
154+
original_code = """def calculate_total(items):
155+
total = 0
156+
for item in items:
157+
total += item.price
158+
return total"""
159+
160+
update_snippet = """def calculate_total(items):
161+
total = 0
162+
for item in items:
163+
total += item.price
164+
return total * 1.1 # Add 10% tax"""
165+
166+
user_message = f"<code>{original_code}</code>\n<update>{update_snippet}</update>"
167+
168+
# Expected response after applying the update
169+
expected_updated_code = """
170+
def calculate_total(items):
171+
total = 0
172+
for item in items:
173+
total += item.price
174+
return total * 1.1 # Add 10% tax
175+
"""
176+
177+
# Mock the HTTP request to the Morph API
178+
respx_mock.post(f"{api_base}/chat/completions").respond(
179+
json={
180+
"id": "chatcmpl-123",
181+
"object": "chat.completion",
182+
"created": 1677652288,
183+
"model": "apply-v1",
184+
"choices": [
185+
{
186+
"index": 0,
187+
"message": {
188+
"role": "assistant",
189+
"content": expected_updated_code,
190+
},
191+
"finish_reason": "stop",
192+
}
193+
],
194+
"usage": {"prompt_tokens": 25, "completion_tokens": 32, "total_tokens": 57},
195+
},
196+
status_code=200
197+
)
198+
199+
# Make the actual API call through LiteLLM
200+
response = completion(
201+
model=model,
202+
messages=[{"role": "user", "content": user_message}],
203+
api_key=api_key,
204+
api_base=api_base
205+
)
206+
207+
# Verify response structure
208+
assert response is not None
209+
assert hasattr(response, "choices")
210+
assert len(response.choices) > 0
211+
assert hasattr(response.choices[0], "message")
212+
assert hasattr(response.choices[0].message, "content")
213+
214+
# Check that the response contains the expected updated code
215+
assert response.choices[0].message.content.strip() == expected_updated_code.strip()

0 commit comments

Comments
 (0)