import pytest import json from io import BytesIO from typing import Dict, List from litellm.router_utils.batch_utils import ( replace_model_in_jsonl, _get_router_metadata_variable_name, InMemoryFile, parse_jsonl_with_embedded_newlines, ) # Fixtures @pytest.fixture def sample_jsonl_data() -> List[Dict]: """Fixture providing sample JSONL data""" return [ { "body": { "model": "gpt-5-mini", "messages": [{"role": "user", "content": "Hello"}], } }, {"body": {"model": "gpt-5.5", "messages": [{"role": "user", "content": "Hi"}]}}, ] @pytest.fixture def sample_jsonl_bytes(sample_jsonl_data) -> bytes: """Fixture providing sample JSONL as bytes""" jsonl_str = "\n".join(json.dumps(line) for line in sample_jsonl_data) return jsonl_str.encode("utf-8") @pytest.fixture def sample_file_like(sample_jsonl_bytes): """Fixture providing a file-like object""" return BytesIO(sample_jsonl_bytes) # Test cases def test_bytes_input(sample_jsonl_bytes): """Test with bytes input""" new_model = "claude-3" result = replace_model_in_jsonl(sample_jsonl_bytes, new_model) assert result is not None assert isinstance(result, InMemoryFile) assert result.name == "modified_file.jsonl" assert result.content_type == "application/jsonl" def test_tuple_input(sample_jsonl_bytes): """Test with tuple input""" new_model = "claude-3" test_tuple = ("test.jsonl", sample_jsonl_bytes, "application/json") result = replace_model_in_jsonl(test_tuple, new_model) assert result is not None assert isinstance(result, InMemoryFile) assert result.name == "modified_file.jsonl" assert result.content_type == "application/jsonl" def test_tuple_with_file_handle_rewrites_model(sample_jsonl_bytes): """Security regression: when the tuple's content element is a file handle (batch uploads stream from the spooled upload handle), the model must still be rewritten. Otherwise a restricted body.model survives unmodified and bypasses the batch model allowlist, which only checks the upload target.""" new_model = "approved-target-model" handle = BytesIO(sample_jsonl_bytes) test_tuple = ("test.jsonl", handle, "application/json") result = replace_model_in_jsonl(test_tuple, new_model) assert isinstance(result, InMemoryFile) rows = [ json.loads(line) for line in result.getvalue().decode("utf-8").splitlines() if line.strip() ] assert rows, "rewrite must produce rows" # every row now carries the rewritten target, not the original (restricted) model assert all(row["body"]["model"] == new_model for row in rows) assert all(row["body"]["model"] != "gpt-5.5" for row in rows) def test_file_like_object(sample_file_like): """Test with file-like object input""" new_model = "claude-3" result = replace_model_in_jsonl(sample_file_like, new_model) assert result is not None assert isinstance(result, InMemoryFile) assert result.name == "modified_file.jsonl" assert result.content_type == "application/jsonl" def test_router_metadata_variable_name(): """Test that the variable name is correct""" assert _get_router_metadata_variable_name(function_name="completion") == "metadata" assert ( _get_router_metadata_variable_name(function_name="batch") == "litellm_metadata" ) assert ( _get_router_metadata_variable_name(function_name="acreate_file") == "litellm_metadata" ) assert ( _get_router_metadata_variable_name(function_name="aget_file") == "litellm_metadata" ) def test_non_json_input(): """Test that replace_model_in_jsonl returns original content for non-JSON input""" from litellm.router_utils.batch_utils import replace_model_in_jsonl # Test with non-JSON string non_json_str = "This is not a JSON string" result = replace_model_in_jsonl(non_json_str, "gpt-4") assert result == non_json_str # Test with non-JSON bytes non_json_bytes = b"This is not JSON bytes" result = replace_model_in_jsonl(non_json_bytes, "gpt-4") assert result == non_json_bytes # Test with non-JSON file-like object from io import BytesIO non_json_file = BytesIO(b"This is not JSON in a file") result = replace_model_in_jsonl(non_json_file, "gpt-4") assert result == non_json_file def test_should_replace_model_in_jsonl(): """Test that should_replace_model_in_jsonl returns the correct value""" from litellm.router_utils.batch_utils import should_replace_model_in_jsonl assert should_replace_model_in_jsonl(purpose="batch") is True assert should_replace_model_in_jsonl(purpose="test") is False assert should_replace_model_in_jsonl(purpose="user_data") is False def test_parse_jsonl_with_embedded_newlines_simple(): """Test parsing simple JSONL without embedded newlines""" content = '{"id": 1, "name": "test"}\n{"id": 2, "name": "test2"}' result = parse_jsonl_with_embedded_newlines(content) assert len(result) == 2 assert result[0] == {"id": 1, "name": "test"} assert result[1] == {"id": 2, "name": "test2"} def test_parse_jsonl_with_embedded_newlines_in_strings(): """Test parsing JSONL with newlines embedded in string values""" content = ( '{"id": 1, "message": "Line 1\\nLine 2\\nLine 3"}\n{"id": 2, "message": "test"}' ) result = parse_jsonl_with_embedded_newlines(content) assert len(result) == 2 assert result[0] == {"id": 1, "message": "Line 1\nLine 2\nLine 3"} assert result[1] == {"id": 2, "message": "test"} def test_parse_jsonl_with_embedded_newlines_real_world_example(): """Test with the real-world example from the Cooler Master Shark X case""" # This simulates the actual problem case from the user's log content = """{"custom_id":"16546277850245725","method":"POST","url":"/v1/chat/completions","body":{"model":"openai-gpt-4o-mini-dp-items-translation-dag","messages":[{"role":"system","content":"Translate the product title and description for an e-commerce marketplace in Saudi Arabia and the UAE. Text may be in English or Arabic.\\n"},{"role":"user","content":"\\nOriginal Title: ```Cooler Master Shark X PC Case```\\nOriginal Description: ```UNIQUE MASTERPIECEShark X is a system that provides an impressive unique alternative to traditional PC systems. Shark X will stand out and can be the ultimate trophy or conversation piece for people looking for a unique setup that stands head and fins above the res.```\\nStore Name: ```geekay```\\n"}]}}""" result = parse_jsonl_with_embedded_newlines(content) assert len(result) == 1 assert result[0]["custom_id"] == "16546277850245725" assert result[0]["method"] == "POST" assert result[0]["body"]["model"] == "openai-gpt-4o-mini-dp-items-translation-dag" assert len(result[0]["body"]["messages"]) == 2 assert "Translate the product title" in result[0]["body"]["messages"][0]["content"] assert ( "Cooler Master Shark X PC Case" in result[0]["body"]["messages"][1]["content"] ) assert "UNIQUE MASTERPIECEShark X" in result[0]["body"]["messages"][1]["content"] def test_parse_jsonl_with_embedded_newlines_multiple_complex_objects(): """Test parsing multiple complex JSON objects with embedded newlines""" content = """{"id":1,"text":"Line 1\\nLine 2"} {"id":2,"nested":{"field":"Value\\nWith\\nNewlines"}} {"id":3,"simple":"test"}""" result = parse_jsonl_with_embedded_newlines(content) assert len(result) == 3 assert result[0]["id"] == 1 assert result[0]["text"] == "Line 1\nLine 2" assert result[1]["id"] == 2 assert result[1]["nested"]["field"] == "Value\nWith\nNewlines" assert result[2]["id"] == 3 assert result[2]["simple"] == "test" def test_parse_jsonl_with_embedded_newlines_no_trailing_newline(): """Test parsing JSONL without trailing newline""" content = '{"id": 1, "name": "test"}' result = parse_jsonl_with_embedded_newlines(content) assert len(result) == 1 assert result[0] == {"id": 1, "name": "test"} def test_parse_jsonl_with_embedded_newlines_empty_string(): """Test parsing empty string""" content = "" result = parse_jsonl_with_embedded_newlines(content) assert len(result) == 0 def test_parse_jsonl_with_embedded_newlines_whitespace_only(): """Test parsing whitespace-only content""" content = " \n \n " result = parse_jsonl_with_embedded_newlines(content) assert len(result) == 0 def test_replace_model_in_jsonl_malformed_middle_row_returns_original(): """Regression: a malformed/truncated middle row must not silently drop the rows that follow it. The streaming rewrite accumulates physical lines into a buffer; a row that never parses poisons the buffer so every later valid row is concatenated into it and dropped. Returning that partial rewrite would ship a truncated batch with no error to the caller. Instead the original content is returned unchanged so the provider rejects the bad batch loudly.""" content = ( b'{"custom_id":"a","body":{"model":"x"}}\n' b'{"custom_id":"b","body":{"model":\n' # truncated, never completes b'{"custom_id":"c","body":{"model":"x"}}\n' ) result = replace_model_in_jsonl(content, "new-model") assert ( result == content ), "must return the original unchanged, not a partial rewrite" def test_replace_model_in_jsonl_malformed_row_seekable_handle_rewound(): """When the source is a seekable handle that gets consumed during the failed rewrite, it must be rewound to 0 so the caller can re-read the full original.""" content = ( b'{"custom_id":"a","body":{"model":"x"}}\n' b'{"custom_id":"b","body":{"model":\n' b'{"custom_id":"c","body":{"model":"x"}}\n' ) handle = BytesIO(content) result = replace_model_in_jsonl(handle, "new-model") assert result is handle assert handle.read() == content, "handle must be rewound for the caller to re-read" def test_replace_model_in_jsonl_multi_row_rewrites_every_model(): """Happy path: a well-formed multi-row file gets every row's model rewritten and no row is dropped.""" content = ( b'{"custom_id":"a","body":{"model":"old1"}}\n' b'{"custom_id":"b","body":{"model":"old2"}}\n' b'{"custom_id":"c","body":{"model":"old3"}}\n' ) result = replace_model_in_jsonl(content, "new-model") assert isinstance(result, InMemoryFile) rows = [ json.loads(line) for line in result.getvalue().decode("utf-8").splitlines() if line.strip() ] assert [row["custom_id"] for row in rows] == ["a", "b", "c"] assert all(row["body"]["model"] == "new-model" for row in rows) def test_replace_model_in_jsonl_with_embedded_newlines(): """Test that replace_model_in_jsonl works correctly with embedded newlines in content""" # Create a JSONL with embedded newlines in the message content jsonl_data = { "custom_id": "test123", "body": { "model": "old-model", "messages": [ {"role": "user", "content": "This is a message\nwith multiple\nlines"} ], }, } jsonl_bytes = json.dumps(jsonl_data).encode("utf-8") new_model = "new-model" result = replace_model_in_jsonl(jsonl_bytes, new_model) assert isinstance(result, InMemoryFile) # Read and parse the result result_content = result.read().decode("utf-8") result_json = json.loads(result_content) # Verify the model was replaced assert result_json["body"]["model"] == "new-model" # Verify the content with newlines is preserved assert ( result_json["body"]["messages"][0]["content"] == "This is a message\nwith multiple\nlines" ) assert result_json["custom_id"] == "test123"