mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-28 06:23:07 +00:00
Merge f0eed8c56b into the batch line validation rework
Keep the strict object-name check for every MIME type and name the file from the first record. Naming checks only that each line is a JSON object, so a file already in Bedrock format and sent with a MIME type that skips conversion still uploads unchanged.
This commit is contained in:
commit
fbc0239679
2 changed files with 69 additions and 21 deletions
|
|
@ -375,11 +375,15 @@ def _json_object(value: object) -> Mapping[str, object] | None:
|
|||
return value if isinstance(value, dict) else None
|
||||
|
||||
|
||||
def _batch_record_shape_problem(record: object) -> str | None:
|
||||
record_object: Final = _json_object(record)
|
||||
if record_object is None:
|
||||
return "is not a JSON object"
|
||||
body: Final = _json_object(record_object.get("body"))
|
||||
def _batch_json_object(line_number: int, line: str) -> _OpenAIBatchRecord:
|
||||
record: Final = _load_batch_line(line_number, line)
|
||||
if _json_object(record) is None:
|
||||
raise _batch_line_error(line_number, "is not a JSON object")
|
||||
return record
|
||||
|
||||
|
||||
def _batch_body_problem(record: Mapping[str, object]) -> str | None:
|
||||
body: Final = _json_object(record.get("body"))
|
||||
if body is None:
|
||||
return "has a body that is missing or not a JSON object"
|
||||
if "model" in body and not isinstance(body["model"], str):
|
||||
|
|
@ -388,8 +392,8 @@ def _batch_record_shape_problem(record: object) -> str | None:
|
|||
|
||||
|
||||
def _batch_input_line(line_number: int, line: str) -> _BatchInputLine:
|
||||
record: Final = _load_batch_line(line_number, line)
|
||||
problem: Final = _batch_record_shape_problem(record)
|
||||
record: Final = _batch_json_object(line_number, line)
|
||||
problem: Final = _batch_body_problem(record)
|
||||
if problem is not None:
|
||||
raise _batch_line_error(line_number, problem)
|
||||
return _BatchInputLine(line_number=line_number, record=record)
|
||||
|
|
@ -404,14 +408,15 @@ def _parse_batch_jsonl(file_content: str) -> tuple[_BatchInputLine, ...]:
|
|||
return tuple(_batch_input_line(line_number, line) for line_number, line in _non_blank_lines(file_content))
|
||||
|
||||
|
||||
def _first_batch_record_model(file_content: str) -> str | None:
|
||||
first_line: Final = next(_non_blank_lines(file_content), None)
|
||||
if first_line is None:
|
||||
return None
|
||||
try:
|
||||
return _batch_input_line(*first_line).record.get("body", {}).get("model", "")
|
||||
except BedrockError:
|
||||
def _batch_file_model(file_content: str) -> str | None:
|
||||
records: Final = tuple(
|
||||
_batch_json_object(line_number, line) for line_number, line in _non_blank_lines(file_content)
|
||||
)
|
||||
if not records:
|
||||
return None
|
||||
body: Final = _json_object(records[0].get("body"))
|
||||
model: Final = body.get("model") if body is not None else None
|
||||
return model if isinstance(model, str) else ""
|
||||
|
||||
|
||||
def _batch_s3_object_name(model: str) -> str:
|
||||
|
|
@ -644,7 +649,7 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig):
|
|||
if purpose == "batch":
|
||||
## 1. If jsonl, check if there's a model name
|
||||
file_content: Final = self._get_content_from_openai_file(extracted_file_data_content)
|
||||
batch_model: Final = _first_batch_record_model(file_content)
|
||||
batch_model: Final = _batch_file_model(file_content)
|
||||
if batch_model is not None:
|
||||
return _batch_s3_object_name(batch_model)
|
||||
|
||||
|
|
|
|||
|
|
@ -4427,13 +4427,33 @@ class TestMalformedBatchJsonl:
|
|||
assert exc_info.value.status_code == 400
|
||||
assert "line 3" in exc_info.value.message
|
||||
|
||||
def test_object_name_inference_still_falls_back_for_a_non_json_file(self):
|
||||
from litellm.llms.bedrock.files.transformation import BedrockFilesConfig
|
||||
@pytest.mark.parametrize("content_type", ["application/jsonl", "application/x-jsonl"])
|
||||
@pytest.mark.parametrize(
|
||||
"file_content",
|
||||
[_MALFORMED_BATCH_JSONL, f"{_VALID_BATCH_LINE}\n\n[1, 2]\n".encode()],
|
||||
ids=["invalid-json", "array-line"],
|
||||
)
|
||||
def test_create_file_rejects_a_malformed_line_before_uploading(self, content_type: str, file_content: bytes):
|
||||
"""application/x-jsonl is not on the batch MIME allowlist, so the upload skips conversion for it; the line
|
||||
check must still run so the broken file is rejected instead of landing in S3 unchanged."""
|
||||
from litellm.llms.bedrock.common_utils import BedrockError
|
||||
|
||||
object_name = BedrockFilesConfig().get_object_name(
|
||||
{"content": b"just some text, not jsonl", "filename": "notes.txt"}, purpose="batch"
|
||||
)
|
||||
assert object_name.endswith("notes.txt")
|
||||
with respx.mock as router:
|
||||
any_request: Final = router.route()
|
||||
with pytest.raises(BedrockError) as exc_info:
|
||||
litellm.create_file(
|
||||
file=("batch.jsonl", file_content, content_type),
|
||||
purpose="batch",
|
||||
custom_llm_provider="bedrock",
|
||||
model="bedrock/amazon.nova-pro-v1:0",
|
||||
s3_bucket_name="litellm-batch-bucket",
|
||||
aws_region_name="us-west-2",
|
||||
aws_access_key_id="test-key-id",
|
||||
aws_secret_access_key="test-secret",
|
||||
)
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "line 3" in exc_info.value.message
|
||||
assert not any_request.called
|
||||
|
||||
|
||||
_VALID_BATCH_RECORD: Final = json.loads(_VALID_BATCH_LINE)
|
||||
|
|
@ -4534,3 +4554,26 @@ async def test_create_file_accepts_a_batch_file_with_a_utf8_byte_order_mark(resp
|
|||
uploaded_record: Final = json.loads(put_route.calls.last.request.content)
|
||||
assert uploaded_record["recordId"] == _VALID_BATCH_RECORD["custom_id"]
|
||||
assert "/litellm-bedrock-files-amazon.nova-pro-v1-0-" in str(put_route.calls.last.request.url)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(httpx_transport.__name__)
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_file_uploads_a_bedrock_format_batch_file_unchanged_when_its_mime_type_skips_conversion(
|
||||
respx_mock: respx.MockRouter,
|
||||
) -> None:
|
||||
put_route: Final = respx_mock.put(url__regex=r"https://s3\..+").respond(status_code=200)
|
||||
bedrock_line: Final = json.dumps(
|
||||
{"recordId": "req-1", "modelInput": {"messages": [{"role": "user", "content": [{"text": "Hello"}]}]}}
|
||||
)
|
||||
|
||||
await litellm.acreate_file(
|
||||
file=("batch.jsonl", f"{bedrock_line}\n".encode(), "application/x-jsonl"),
|
||||
purpose="batch",
|
||||
custom_llm_provider="bedrock",
|
||||
s3_bucket_name="litellm-batch-bucket",
|
||||
aws_region_name="us-west-2",
|
||||
aws_access_key_id="test-access-key",
|
||||
aws_secret_access_key="test-secret-key",
|
||||
)
|
||||
|
||||
assert put_route.calls.last.request.content == f"{bedrock_line}\n".encode()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue