mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-25 01:02:15 +00:00
Merge pull request #42115 from BerriAI/litellm_migrate_tests_p10
test: migrate nvidia, oci, ocr, oobabooga and openai legacy tests to tests/unit
This commit is contained in:
commit
e08df02ebe
25 changed files with 0 additions and 236 deletions
|
|
@ -62,19 +62,6 @@ def test_resample_16khz_mono_passes_through_int16_bytes_match_length():
|
|||
assert resampled.duration_seconds == pytest.approx(1.0, abs=0.001)
|
||||
|
||||
|
||||
def test_resample_preserves_int16_clip_range():
|
||||
sample_rate = 16000
|
||||
samples = np.array([2.0, -2.0, 0.0, 1.0], dtype=np.float32)
|
||||
wav_in = _wav_bytes(samples, sample_rate)
|
||||
|
||||
resampled = resample_to_riva_pcm(wav_in)
|
||||
|
||||
decoded = np.frombuffer(resampled.pcm_bytes, dtype="<i2")
|
||||
# Anything outside [-1, 1] should clip to int16 boundary.
|
||||
assert decoded.max() <= 32767
|
||||
assert decoded.min() >= -32767
|
||||
|
||||
|
||||
def test_unknown_format_raises_clear_error():
|
||||
# 4 random bytes are not valid audio in any container we can decode.
|
||||
with pytest.raises(NvidiaRivaException) as excinfo:
|
||||
|
|
@ -922,91 +922,6 @@ class TestOCISignerSupport:
|
|||
assert wrapper.path_url == "/api/v1/chat"
|
||||
|
||||
|
||||
class TestOCISplitChunks:
|
||||
"""
|
||||
Unit tests for the SSE split_chunks helpers used in sync and async streaming.
|
||||
|
||||
These validate the fix for:
|
||||
- Sync: JSONDecodeError when iter_text() returns chunks spanning multiple events
|
||||
- Async: whitespace-only chunks being yielded before stripping (Greptile P2)
|
||||
"""
|
||||
|
||||
def _run_sync_split(self, raw_chunks):
|
||||
"""Invoke the sync split_chunks logic directly (extracted for testability)."""
|
||||
results = []
|
||||
for item in raw_chunks:
|
||||
for chunk in item.split("\n\n"):
|
||||
stripped = chunk.strip()
|
||||
if stripped:
|
||||
results.append(stripped)
|
||||
return results
|
||||
|
||||
async def _run_async_split(self, raw_chunks):
|
||||
"""Invoke the async split_chunks logic directly."""
|
||||
results = []
|
||||
|
||||
async def _gen():
|
||||
for c in raw_chunks:
|
||||
yield c
|
||||
|
||||
async for item in _gen():
|
||||
for chunk in item.split("\n\n"):
|
||||
stripped = chunk.strip()
|
||||
if stripped:
|
||||
results.append(stripped)
|
||||
return results
|
||||
|
||||
def test_sync_single_event_per_chunk(self):
|
||||
"""Normal case: one SSE event per iter_text() chunk."""
|
||||
chunks = ['data: {"text":"hello"}', 'data: {"text":"world"}']
|
||||
assert self._run_sync_split(chunks) == [
|
||||
'data: {"text":"hello"}',
|
||||
'data: {"text":"world"}',
|
||||
]
|
||||
|
||||
def test_sync_multiple_events_in_one_chunk(self):
|
||||
"""iter_text() returns two SSE events concatenated — must be split."""
|
||||
chunks = ['data: {"text":"a"}\n\ndata: {"text":"b"}']
|
||||
assert self._run_sync_split(chunks) == [
|
||||
'data: {"text":"a"}',
|
||||
'data: {"text":"b"}',
|
||||
]
|
||||
|
||||
def test_sync_whitespace_only_chunks_discarded(self):
|
||||
"""Whitespace between events must not be yielded."""
|
||||
chunks = ["data: {}\n\n \n\ndata: {}"]
|
||||
result = self._run_sync_split(chunks)
|
||||
assert result == ["data: {}", "data: {}"]
|
||||
|
||||
def test_sync_empty_string_discarded(self):
|
||||
"""Empty string produced by splitting trailing \\n\\n must be discarded."""
|
||||
chunks = ["data: {}\n\n"]
|
||||
assert self._run_sync_split(chunks) == ["data: {}"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_whitespace_only_chunks_discarded(self):
|
||||
"""
|
||||
Regression test for Greptile P2: async version was checking `if not chunk`
|
||||
BEFORE stripping, so '\\n ' would pass the guard and yield '' downstream,
|
||||
causing ValueError in chunk_creator ('Chunk does not start with data:').
|
||||
"""
|
||||
chunks = ["data: {}\n\n \n\ndata: {}"]
|
||||
result = await self._run_async_split(chunks)
|
||||
assert result == ["data: {}", "data: {}"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_empty_string_discarded(self):
|
||||
"""Trailing \\n\\n must not produce an empty yielded chunk in async path."""
|
||||
chunks = ["data: {}\n\n"]
|
||||
result = await self._run_async_split(chunks)
|
||||
assert result == ["data: {}"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_multiple_events_in_one_chunk(self):
|
||||
"""Async path must split concatenated SSE events just like sync."""
|
||||
chunks = ['data: {"text":"x"}\n\ndata: {"text":"y"}']
|
||||
result = await self._run_async_split(chunks)
|
||||
assert result == ['data: {"text":"x"}', 'data: {"text":"y"}']
|
||||
|
||||
|
||||
class TestOCIProviderEmbeddingConfig:
|
||||
|
|
@ -1026,21 +941,6 @@ class TestOCIProviderEmbeddingConfig:
|
|||
)
|
||||
assert isinstance(config, OCIEmbedConfig)
|
||||
|
||||
def test_no_duplicate_oci_branch(self):
|
||||
"""
|
||||
Ensure utils.py does not contain two separate OCI embedding branches.
|
||||
The dead code was removed in commit 64dfbe2b; this test guards against
|
||||
regression (e.g. a future merge re-introducing it).
|
||||
"""
|
||||
import inspect
|
||||
from litellm.utils import ProviderConfigManager
|
||||
|
||||
source = inspect.getsource(ProviderConfigManager.get_provider_embedding_config)
|
||||
oci_count = source.count("LlmProviders.OCI")
|
||||
assert oci_count == 1, (
|
||||
f"Expected exactly 1 OCI branch in get_provider_embedding_config, found {oci_count}. "
|
||||
"A duplicate dead-code branch may have been reintroduced."
|
||||
)
|
||||
|
||||
|
||||
class TestOCICohereParamMapping:
|
||||
|
|
@ -1586,57 +1486,7 @@ def config():
|
|||
class TestOCIKeyNormalization:
|
||||
"""Tests for OCI private key content normalization."""
|
||||
|
||||
def test_oci_key_with_escaped_newlines(self, config):
|
||||
"""Test that escaped newlines (\\n) are converted to actual newlines."""
|
||||
# Simulate PEM content with escaped newlines (as would come from JSON/UI input)
|
||||
escaped_pem = "-----BEGIN RSA PRIVATE KEY-----\\nMIIEowIBAAKCAQEA...\\n-----END RSA PRIVATE KEY-----"
|
||||
|
||||
optional_params = {
|
||||
"oci_user": "ocid1.user.oc1..test",
|
||||
"oci_fingerprint": "aa:bb:cc:dd",
|
||||
"oci_tenancy": "ocid1.tenancy.oc1..test",
|
||||
"oci_region": "us-ashburn-1",
|
||||
"oci_key": escaped_pem,
|
||||
}
|
||||
|
||||
# We can't fully test signing without a real key, but we can verify
|
||||
# the error message indicates the key was processed (not a type error)
|
||||
with pytest.raises(Exception, match='why-can-t-i-import-my-pem-file for more details\\.') as exc_info:
|
||||
sign_with_manual_credentials(
|
||||
headers={},
|
||||
optional_params=optional_params,
|
||||
request_data={"test": "data"},
|
||||
api_base="https://test.oci.oraclecloud.com/api",
|
||||
)
|
||||
|
||||
# The error should be about key format/loading, not about type
|
||||
# This confirms the string was processed and newlines were normalized
|
||||
error_message = str(exc_info.value)
|
||||
assert "must be a string" not in error_message.lower()
|
||||
|
||||
def test_oci_key_with_crlf_newlines(self, config):
|
||||
"""Test that Windows-style CRLF newlines are normalized to LF."""
|
||||
# Simulate PEM content with CRLF newlines
|
||||
crlf_pem = "-----BEGIN RSA PRIVATE KEY-----\r\nMIIEowIBAAKCAQEA...\r\n-----END RSA PRIVATE KEY-----"
|
||||
|
||||
optional_params = {
|
||||
"oci_user": "ocid1.user.oc1..test",
|
||||
"oci_fingerprint": "aa:bb:cc:dd",
|
||||
"oci_tenancy": "ocid1.tenancy.oc1..test",
|
||||
"oci_region": "us-ashburn-1",
|
||||
"oci_key": crlf_pem,
|
||||
}
|
||||
|
||||
with pytest.raises(Exception, match='why-can-t-i-import-my-pem-file for more details\\.') as exc_info:
|
||||
sign_with_manual_credentials(
|
||||
headers={},
|
||||
optional_params=optional_params,
|
||||
request_data={"test": "data"},
|
||||
api_base="https://test.oci.oraclecloud.com/api",
|
||||
)
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
assert "must be a string" not in error_message.lower()
|
||||
|
||||
def test_oci_key_rejects_non_string_type(self, config):
|
||||
"""Test that non-string oci_key values raise OCIError."""
|
||||
|
|
@ -966,13 +966,6 @@ class TestOCICohereStreaming:
|
|||
completion_stream=mock_stream, model=mock_model, logging_obj=mock_logging
|
||||
)
|
||||
|
||||
def test_cohere_streaming_wrapper_initialization(self):
|
||||
"""Test OCIStreamWrapper initialization"""
|
||||
stream_wrapper = self._create_stream_wrapper()
|
||||
|
||||
# chunk_creator is the public dispatch entry point
|
||||
assert hasattr(stream_wrapper, "chunk_creator")
|
||||
assert callable(stream_wrapper.chunk_creator)
|
||||
|
||||
def test_cohere_streaming_chunk_parsing(self):
|
||||
"""Test parsing of Cohere streaming chunks"""
|
||||
|
|
@ -1003,16 +996,3 @@ class TestOCICohereStreaming:
|
|||
# Test non-JSON chunk
|
||||
with pytest.raises(OCIError, match="Chunk cannot be parsed as JSON"):
|
||||
stream_wrapper.chunk_creator("data: invalid json")
|
||||
|
||||
def test_cohere_streaming_generic_chunk_fallback(self):
|
||||
"""Test fallback to generic chunk handling for non-Cohere chunks"""
|
||||
stream_wrapper = self._create_stream_wrapper()
|
||||
|
||||
# Test generic chunk (no apiFormat or different apiFormat)
|
||||
generic_chunk = {"apiFormat": "GEMINI", "text": "Hello from Gemini"}
|
||||
chunk_data = f"data: {json.dumps(generic_chunk)}"
|
||||
|
||||
# This should fall back to generic handling
|
||||
result = stream_wrapper.chunk_creator(chunk_data)
|
||||
# The exact structure depends on the generic handler implementation
|
||||
assert hasattr(result, "choices")
|
||||
|
|
@ -450,15 +450,3 @@ class TestGpt5MaxCompletionTokens:
|
|||
)
|
||||
assert out.get("maxTokens") == 64
|
||||
assert "maxCompletionTokens" not in out
|
||||
|
||||
def test_payload_serializes_max_completion_tokens(self):
|
||||
from litellm.types.llms.oci import OCIChatRequestPayload
|
||||
|
||||
payload = OCIChatRequestPayload(
|
||||
apiFormat="GENERIC",
|
||||
messages=[],
|
||||
maxCompletionTokens=64,
|
||||
)
|
||||
dumped = payload.model_dump(exclude_none=True)
|
||||
assert dumped["maxCompletionTokens"] == 64
|
||||
assert "maxTokens" not in dumped
|
||||
|
|
@ -269,28 +269,6 @@ class TestOCIEmbedConfig:
|
|||
assert result.model == "cohere.embed-v3.0"
|
||||
assert result.usage.prompt_tokens == 10
|
||||
|
||||
def test_transform_response_no_usage(self):
|
||||
cfg = self._config()
|
||||
model_response = EmbeddingResponse()
|
||||
raw = self._mock_response(
|
||||
200,
|
||||
{
|
||||
"embeddings": [[0.1]],
|
||||
"modelId": "cohere.embed-v3.0",
|
||||
"modelVersion": "3.0.0",
|
||||
},
|
||||
)
|
||||
result = cfg.transform_embedding_response(
|
||||
model="cohere.embed-v3.0",
|
||||
raw_response=raw,
|
||||
model_response=model_response,
|
||||
logging_obj=MagicMock(),
|
||||
api_key=None,
|
||||
request_data={},
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
)
|
||||
assert len(result.data) == 1
|
||||
|
||||
def test_transform_response_http_error_raises(self):
|
||||
cfg = self._config()
|
||||
|
|
@ -545,25 +545,6 @@ class TestOpenAIChatCompletionsHandlerToolCallsInput:
|
|||
assert data["messages"][0]["content"] == "HELLO"
|
||||
assert data["messages"][1]["content"] == "HI THERE!"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_tool_calls_list(self):
|
||||
"""Test that empty tool_calls list is handled correctly"""
|
||||
handler = OpenAIChatCompletionsHandler()
|
||||
guardrail = MockGuardrail()
|
||||
|
||||
data = {
|
||||
"messages": [
|
||||
{"role": "assistant", "content": "Hello", "tool_calls": []},
|
||||
]
|
||||
}
|
||||
|
||||
# Process the input
|
||||
await handler.process_input_messages(data, guardrail)
|
||||
|
||||
# Verify empty tool_calls doesn't cause issues
|
||||
assert guardrail.last_inputs is not None
|
||||
tool_calls = guardrail.last_inputs.get("tool_calls", [])
|
||||
assert len(tool_calls) == 0
|
||||
|
||||
|
||||
class TestOpenAIChatCompletionsHandlerToolCallsOutput:
|
||||
Loading…
Add table
Reference in a new issue