* fix(convert_dict_to_response.py): handle None values in usage field for gpt-image-1 * test: add tests for handling None and partial values in usage fields for gpt-image-1 responses
224 lines
7.7 KiB
Python
224 lines
7.7 KiB
Python
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime
|
|
|
|
sys.path.insert(
|
|
0, os.path.abspath("../../")
|
|
) # Adds the parent directory to the system path
|
|
|
|
import litellm
|
|
import pytest
|
|
from datetime import timedelta
|
|
from litellm.types.utils import ImageResponse, ImageObject
|
|
from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import (
|
|
LiteLLMResponseObjectHandler,
|
|
)
|
|
|
|
|
|
def test_convert_to_image_response_basic():
|
|
# Test basic conversion with minimal input
|
|
response_dict = {
|
|
"created": 1234567890,
|
|
"data": [{"url": "http://example.com/image.jpg"}],
|
|
}
|
|
|
|
result = LiteLLMResponseObjectHandler.convert_to_image_response(response_dict)
|
|
|
|
assert isinstance(result, ImageResponse)
|
|
assert result.created == 1234567890
|
|
assert result.data[0].url == "http://example.com/image.jpg"
|
|
|
|
|
|
def test_convert_to_image_response_with_hidden_params():
|
|
# Test with hidden params
|
|
response_dict = {
|
|
"created": 1234567890,
|
|
"data": [{"url": "http://example.com/image.jpg"}],
|
|
}
|
|
hidden_params = {"api_key": "test_key"}
|
|
|
|
result = LiteLLMResponseObjectHandler.convert_to_image_response(
|
|
response_dict, hidden_params=hidden_params
|
|
)
|
|
|
|
assert result._hidden_params == {"api_key": "test_key"}
|
|
|
|
|
|
def test_convert_to_image_response_multiple_images():
|
|
# Test handling multiple images in response
|
|
response_dict = {
|
|
"created": 1234567890,
|
|
"data": [
|
|
{"url": "http://example.com/image1.jpg"},
|
|
{"url": "http://example.com/image2.jpg"},
|
|
],
|
|
}
|
|
|
|
result = LiteLLMResponseObjectHandler.convert_to_image_response(response_dict)
|
|
|
|
assert len(result.data) == 2
|
|
assert result.data[0].url == "http://example.com/image1.jpg"
|
|
assert result.data[1].url == "http://example.com/image2.jpg"
|
|
|
|
|
|
def test_convert_to_image_response_with_b64_json():
|
|
# Test handling b64_json in response
|
|
response_dict = {
|
|
"created": 1234567890,
|
|
"data": [{"b64_json": "base64encodedstring"}],
|
|
}
|
|
|
|
result = LiteLLMResponseObjectHandler.convert_to_image_response(response_dict)
|
|
|
|
assert result.data[0].b64_json == "base64encodedstring"
|
|
|
|
|
|
def test_convert_to_image_response_with_extra_fields():
|
|
response_dict = {
|
|
"created": 1234567890,
|
|
"data": [
|
|
{
|
|
"url": "http://example.com/image1.jpg",
|
|
"content_filter_results": {"category": "violence", "flagged": True},
|
|
},
|
|
{
|
|
"url": "http://example.com/image2.jpg",
|
|
"content_filter_results": {"category": "violence", "flagged": True},
|
|
},
|
|
],
|
|
}
|
|
|
|
result = LiteLLMResponseObjectHandler.convert_to_image_response(response_dict)
|
|
|
|
assert result.data[0].url == "http://example.com/image1.jpg"
|
|
assert result.data[1].url == "http://example.com/image2.jpg"
|
|
|
|
|
|
def test_convert_to_image_response_with_extra_fields_2():
|
|
"""
|
|
Date from a non-OpenAI API could have some obscure field in addition to the expected ones. This should not break the conversion.
|
|
"""
|
|
response_dict = {
|
|
"created": 1234567890,
|
|
"data": [
|
|
{
|
|
"url": "http://example.com/image1.jpg",
|
|
"very_obscure_field": "some_value",
|
|
},
|
|
{
|
|
"url": "http://example.com/image2.jpg",
|
|
"very_obscure_field2": "some_other_value",
|
|
},
|
|
],
|
|
}
|
|
|
|
result = LiteLLMResponseObjectHandler.convert_to_image_response(response_dict)
|
|
|
|
assert result.data[0].url == "http://example.com/image1.jpg"
|
|
assert result.data[1].url == "http://example.com/image2.jpg"
|
|
|
|
|
|
def test_convert_to_image_response_with_none_usage_fields():
|
|
"""
|
|
Test handling of None values in usage fields, specifically for gpt-image-1 responses.
|
|
|
|
This test verifies the fix for the bug where gpt-image-1 returns None values
|
|
for usage statistics fields, which caused Pydantic validation errors.
|
|
The fix should clean these None values and let ImageResponse constructor
|
|
handle the default values.
|
|
"""
|
|
response_dict = {
|
|
"created": 1234567890,
|
|
"data": [{"b64_json": "base64encodedstring"}],
|
|
"usage": {
|
|
"input_tokens": None, # gpt-image-1 returns None instead of integer
|
|
"input_tokens_details": None, # gpt-image-1 returns None instead of object
|
|
"output_tokens": None, # gpt-image-1 returns None instead of integer
|
|
"total_tokens": None, # gpt-image-1 returns None instead of integer
|
|
}
|
|
}
|
|
|
|
# This should not raise a ValidationError
|
|
result = LiteLLMResponseObjectHandler.convert_to_image_response(response_dict)
|
|
|
|
assert isinstance(result, ImageResponse)
|
|
assert result.created == 1234567890
|
|
assert result.data[0].b64_json == "base64encodedstring"
|
|
|
|
# Usage should be properly initialized with default values
|
|
assert result.usage is not None
|
|
assert result.usage.input_tokens == 0
|
|
assert result.usage.output_tokens == 0
|
|
assert result.usage.total_tokens == 0
|
|
assert result.usage.input_tokens_details is not None
|
|
assert result.usage.input_tokens_details.image_tokens == 0
|
|
assert result.usage.input_tokens_details.text_tokens == 0
|
|
|
|
|
|
def test_convert_to_image_response_with_partial_none_usage_fields():
|
|
"""
|
|
Test handling of mixed None and valid values in usage fields.
|
|
"""
|
|
response_dict = {
|
|
"created": 1234567890,
|
|
"data": [{"b64_json": "base64encodedstring"}],
|
|
"usage": {
|
|
"input_tokens": 10, # Valid value
|
|
"input_tokens_details": None, # None value (should be cleaned)
|
|
"output_tokens": None, # None value (should be cleaned)
|
|
"total_tokens": 10, # Valid value
|
|
}
|
|
}
|
|
|
|
# This should not raise a ValidationError
|
|
result = LiteLLMResponseObjectHandler.convert_to_image_response(response_dict)
|
|
|
|
assert isinstance(result, ImageResponse)
|
|
assert result.created == 1234567890
|
|
assert result.data[0].b64_json == "base64encodedstring"
|
|
|
|
# Usage should be properly initialized with defaults where needed
|
|
# Valid values should be preserved, None values should be cleaned and use defaults
|
|
assert result.usage is not None
|
|
assert result.usage.input_tokens == 10 # Valid value should be preserved
|
|
assert result.usage.output_tokens == 0 # None value should become 0
|
|
assert result.usage.total_tokens == 10 # Calculated as input_tokens + output_tokens (10 + 0)
|
|
assert result.usage.input_tokens_details is not None
|
|
assert result.usage.input_tokens_details.image_tokens == 0
|
|
assert result.usage.input_tokens_details.text_tokens == 0
|
|
|
|
|
|
def test_convert_to_image_response_with_valid_usage_fields():
|
|
"""
|
|
Test that valid usage fields are preserved correctly.
|
|
"""
|
|
response_dict = {
|
|
"created": 1234567890,
|
|
"data": [{"b64_json": "base64encodedstring"}],
|
|
"usage": {
|
|
"input_tokens": 50,
|
|
"input_tokens_details": {
|
|
"image_tokens": 30,
|
|
"text_tokens": 20,
|
|
},
|
|
"output_tokens": 10,
|
|
"total_tokens": 60,
|
|
}
|
|
}
|
|
|
|
result = LiteLLMResponseObjectHandler.convert_to_image_response(response_dict)
|
|
|
|
assert isinstance(result, ImageResponse)
|
|
assert result.created == 1234567890
|
|
assert result.data[0].b64_json == "base64encodedstring"
|
|
|
|
# Valid usage fields should be preserved
|
|
assert result.usage is not None
|
|
assert result.usage.input_tokens == 50
|
|
assert result.usage.output_tokens == 10
|
|
assert result.usage.total_tokens == 60
|
|
assert result.usage.input_tokens_details is not None
|
|
assert result.usage.input_tokens_details.image_tokens == 30
|
|
assert result.usage.input_tokens_details.text_tokens == 20
|