diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index cf742bc52cb7..527ae4a9d49a 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -11,6 +11,7 @@ from litellm.types.router import GenericLiteLLMParams from ..common_utils import OpenAIError +from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import _safe_convert_created_field if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj @@ -85,6 +86,7 @@ def transform_response_api_response( """No transform applied since outputs are in OpenAI spec already""" try: raw_response_json = raw_response.json() + raw_response_json["created_at"] = _safe_convert_created_field(raw_response_json["created_at"]) except Exception: raise OpenAIError( message=raw_response.text, status_code=raw_response.status_code diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index 9a9a426743a9..c3581458aa60 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -1010,7 +1010,7 @@ class ResponseAPIUsage(BaseLiteLLMOpenAIResponseObject): class ResponsesAPIResponse(BaseLiteLLMOpenAIResponseObject): id: str - created_at: float + created_at: int error: Optional[dict] incomplete_details: Optional[IncompleteDetails] instructions: Optional[str] @@ -1034,6 +1034,7 @@ class ResponsesAPIResponse(BaseLiteLLMOpenAIResponseObject): truncation: Optional[Literal["auto", "disabled"]] usage: Optional[ResponseAPIUsage] user: Optional[str] + store: Optional[bool] = None # Define private attributes using PrivateAttr _hidden_params: dict = PrivateAttr(default_factory=dict) diff --git a/tests/llm_responses_api_testing/base_responses_api.py b/tests/llm_responses_api_testing/base_responses_api.py index 0519b8229323..c2eedb458375 100644 --- a/tests/llm_responses_api_testing/base_responses_api.py +++ b/tests/llm_responses_api_testing/base_responses_api.py @@ -1,4 +1,3 @@ - import httpx import json import pytest @@ -51,8 +50,8 @@ def validate_responses_api_response(response, final_chunk: bool = False): response["id"], str ), "Response should have a string 'id' field" assert "created_at" in response and isinstance( - response["created_at"], (int, float) - ), "Response should have a numeric 'created_at' field" + response["created_at"], int + ), "Response should have an integer 'created_at' field" assert "output" in response and isinstance( response["output"], list ), "Response should have a list 'output' field" @@ -80,6 +79,7 @@ def validate_responses_api_response(response, final_chunk: bool = False): "truncation": (str, type(None)), "usage": ResponseAPIUsage, "user": (str, type(None)), + "store": (bool, type(None)), } if final_chunk is False: optional_fields["usage"] = type(None) diff --git a/tests/llm_responses_api_testing/test_openai_responses_api.py b/tests/llm_responses_api_testing/test_openai_responses_api.py index 981b7efc77b6..b23186a13a1d 100644 --- a/tests/llm_responses_api_testing/test_openai_responses_api.py +++ b/tests/llm_responses_api_testing/test_openai_responses_api.py @@ -4,6 +4,11 @@ import asyncio from typing import Optional, cast from unittest.mock import patch, AsyncMock +import httpx +from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig +from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj +import time +import json sys.path.insert(0, os.path.abspath("../..")) import litellm @@ -1152,3 +1157,140 @@ def test_mcp_tools_with_responses_api(): print(response_with_mcp_call) +@pytest.mark.asyncio +async def test_openai_responses_api_field_types(): + """Test that specific fields in the response have the correct types""" + litellm._turn_on_debug() + litellm.set_verbose = True + + # Test with store=True + response = await litellm.aresponses( + model="gpt-4o", + input="hi", + ) + + # Verify created_at is an integer + assert isinstance(response.created_at, int), "created_at should be an integer" + + # Verify store field is present and matches input + assert hasattr(response, "store"), "store field should be present" + assert response.store is True, "store field should match input value" + + # Test without store parameter + response_without_store = await litellm.aresponses( + model="gpt-4o", + input="hi" + ) + + # Verify created_at is still an integer + assert isinstance(response_without_store.created_at, int), "created_at should be an integer" + + # Verify store field is present but None when not specified + assert hasattr(response_without_store, "store"), "store field should be present" + + +@pytest.mark.asyncio +async def test_store_field_transformation(): + """Test store field transformation with mocked API responses""" + config = OpenAIResponsesAPIConfig() + + # Initialize logging object with required parameters + logging_obj = LiteLLMLoggingObj( + model="gpt-4o", + messages=[], + stream=False, + call_type="aresponses", + start_time=time.time(), + litellm_call_id="test-call-id", + function_id="test-function-id" + ) + + # Base response data with all required fields + base_response = { + "id": "test_id", + "created_at": 1751443898, + "model": "gpt-4o", + "object": "response", + "output": [{"type": "message", "id": "msg_1", "status": "completed", "role": "assistant", "content": [{"type": "output_text", "text": "Hello", "annotations": []}]}], + "parallel_tool_calls": True, + "tool_choice": "auto", + "tools": [], + "error": None, + "incomplete_details": None, + "instructions": "test instructions", + "metadata": {}, + "temperature": 0.7, + "top_p": 1.0, + "max_output_tokens": 100, + "previous_response_id": None, + "reasoning": None, + "status": "completed", + "text": None, + "truncation": "auto", + "usage": {"input_tokens": 10, "output_tokens": 20, "total_tokens": 30}, + "user": "test_user" + } + + # Test case 1: API returns store=True + mock_response_store_true = httpx.Response( + status_code=200, + content=json.dumps({**base_response, "store": True}).encode() + ) + + # Test case 2: API returns store=False + mock_response_store_false = httpx.Response( + status_code=200, + content=json.dumps({**base_response, "store": False}).encode() + ) + + # Test case 3: API returns store=null + mock_response_store_null = httpx.Response( + status_code=200, + content=json.dumps({**base_response, "store": None}).encode() + ) + + # Test case 4: API omits store field + mock_response_no_store = httpx.Response( + status_code=200, + content=json.dumps(base_response).encode() + ) + + # Test when store=True in request + logging_obj.optional_params = {"store": True} + response = config.transform_response_api_response( + model="gpt-4o", + raw_response=mock_response_store_true, + logging_obj=logging_obj + ) + assert response.store is True, "store should be True when specified in request and API returns True" + + # Test when store=False in request + logging_obj.optional_params = {"store": False} + response = config.transform_response_api_response( + model="gpt-4o", + raw_response=mock_response_store_false, + logging_obj=logging_obj + ) + assert response.store is False, "store should be False when specified in request and API returns False" + + # Test when store not in request but API returns null + response = config.transform_response_api_response( + model="gpt-4o", + raw_response=mock_response_store_null, + logging_obj=logging_obj + ) + assert response.store is None, "store should be None when not specified in request and API returns null" + + # Test when store not in request and API omits store field + response = config.transform_response_api_response( + model="gpt-4o", + raw_response=mock_response_no_store, + logging_obj=logging_obj + ) + assert response.store is None, "store should be None when not specified in request and API omits store" + + # Verify created_at is always converted to integer + assert isinstance(response.created_at, int), "created_at should always be converted to integer" + assert response.created_at == 1751443898, "created_at should maintain the same value after conversion" + +