Python: Fix: AgentRunResponse.created_at returns local datetime labeled as UTC (#2590)

* Initial plan

* Fix created_at to use UTC datetime and add unit tests

Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com>

* Address code review feedback: move datetime imports to top of test files

Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com>

* Add UTC timestamp test for OpenAIResponsesClient._create_response_content

Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com>

* Fix lint issue

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: moonbox3 <35585003+moonbox3@users.noreply.github.com>
Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
This commit is contained in:
Copilot
2025-12-04 14:09:52 +09:00
committed by GitHub
Unverified
parent ae06c9e4bb
commit 292a2078fa
6 changed files with 175 additions and 7 deletions
@@ -5,7 +5,7 @@ import logging
import uuid
from collections.abc import AsyncIterable
from dataclasses import dataclass
from datetime import datetime
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, ClassVar, TypedDict, cast
from agent_framework import (
@@ -269,7 +269,7 @@ class WorkflowAgent(BaseAgent):
author_name=self.name,
response_id=response_id,
message_id=str(uuid.uuid4()),
created_at=datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
created_at=datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
)
case _:
# Ignore workflow-internal events
@@ -3,7 +3,7 @@
import json
import sys
from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, MutableMapping, MutableSequence, Sequence
from datetime import datetime
from datetime import datetime, timezone
from itertools import chain
from typing import Any, TypeVar
@@ -214,7 +214,7 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
messages.append(ChatMessage(role="assistant", contents=contents))
return ChatResponse(
response_id=response.id,
created_at=datetime.fromtimestamp(response.created).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
created_at=datetime.fromtimestamp(response.created, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
usage_details=self._usage_details_from_openai(response.usage) if response.usage else None,
messages=messages,
model_id=response.model,
@@ -249,7 +249,7 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
if text_content := self._parse_text_from_choice(choice):
contents.append(text_content)
return ChatResponseUpdate(
created_at=datetime.fromtimestamp(chunk.created).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
created_at=datetime.fromtimestamp(chunk.created, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
contents=contents,
role=Role.ASSISTANT,
model_id=chunk.model,
@@ -1,7 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, MutableMapping, MutableSequence, Sequence
from datetime import datetime
from datetime import datetime, timezone
from itertools import chain
from typing import Any, TypeVar
@@ -815,7 +815,9 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
response_message = ChatMessage(role="assistant", contents=contents)
args: dict[str, Any] = {
"response_id": response.id,
"created_at": datetime.fromtimestamp(response.created_at).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
"created_at": datetime.fromtimestamp(response.created_at, tz=timezone.utc).strftime(
"%Y-%m-%dT%H:%M:%S.%fZ"
),
"messages": response_message,
"model_id": response.model,
"additional_properties": metadata,
@@ -2,6 +2,7 @@
import base64
from collections.abc import AsyncIterable
from datetime import datetime, timezone
from typing import Any
import pytest
@@ -936,6 +937,52 @@ def test_agent_run_response_update_str_method(text_content: TextContent) -> None
assert str(update) == "Test content"
def test_agent_run_response_update_created_at() -> None:
"""Test that AgentRunResponseUpdate properly handles created_at timestamps."""
# Test with a properly formatted UTC timestamp
utc_timestamp = "2024-12-01T00:31:30.000000Z"
update = AgentRunResponseUpdate(
contents=[TextContent(text="test")],
role=Role.ASSISTANT,
created_at=utc_timestamp,
)
assert update.created_at == utc_timestamp
assert update.created_at.endswith("Z"), "Timestamp should end with 'Z' for UTC"
# Verify that we can generate a proper UTC timestamp
now_utc = datetime.now(tz=timezone.utc)
formatted_utc = now_utc.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
update_with_now = AgentRunResponseUpdate(
contents=[TextContent(text="test")],
role=Role.ASSISTANT,
created_at=formatted_utc,
)
assert update_with_now.created_at == formatted_utc
assert update_with_now.created_at.endswith("Z")
def test_agent_run_response_created_at() -> None:
"""Test that AgentRunResponse properly handles created_at timestamps."""
# Test with a properly formatted UTC timestamp
utc_timestamp = "2024-12-01T00:31:30.000000Z"
response = AgentRunResponse(
messages=[ChatMessage(role=Role.ASSISTANT, text="Hello")],
created_at=utc_timestamp,
)
assert response.created_at == utc_timestamp
assert response.created_at.endswith("Z"), "Timestamp should end with 'Z' for UTC"
# Verify that we can generate a proper UTC timestamp
now_utc = datetime.now(tz=timezone.utc)
formatted_utc = now_utc.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
response_with_now = AgentRunResponse(
messages=[ChatMessage(role=Role.ASSISTANT, text="Hello")],
created_at=formatted_utc,
)
assert response_with_now.created_at == formatted_utc
assert response_with_now.created_at.endswith("Z")
# region ErrorContent
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
from copy import deepcopy
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@@ -370,3 +371,75 @@ async def test_get_streaming_no_stream(
messages=chat_history,
)
]
# region UTC Timestamp Tests
def test_chat_response_created_at_uses_utc(openai_unit_test_env: dict[str, str]):
"""Test that ChatResponse.created_at uses UTC timestamp, not local time.
This is a regression test for the issue where created_at was using local time
but labeling it as UTC (with 'Z' suffix).
"""
from agent_framework import ChatOptions
# Use a specific Unix timestamp: 1733011890 = 2024-12-01T00:31:30Z (UTC)
# This ensures we test that the timestamp is actually converted to UTC
utc_timestamp = 1733011890
mock_response = ChatCompletion(
id="test_id",
choices=[
Choice(index=0, message=ChatCompletionMessage(content="test", role="assistant"), finish_reason="stop")
],
created=utc_timestamp,
model="test",
object="chat.completion",
)
client = OpenAIChatClient()
response = client._create_chat_response(mock_response, ChatOptions())
# Verify that created_at is correctly formatted as UTC
assert response.created_at is not None
assert response.created_at.endswith("Z"), "Timestamp should end with 'Z' for UTC"
# Parse the timestamp and verify it matches UTC time
expected_utc_time = datetime.fromtimestamp(utc_timestamp, tz=timezone.utc)
expected_formatted = expected_utc_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
assert response.created_at == expected_formatted, (
f"Expected UTC timestamp {expected_formatted}, got {response.created_at}"
)
def test_chat_response_update_created_at_uses_utc(openai_unit_test_env: dict[str, str]):
"""Test that ChatResponseUpdate.created_at uses UTC timestamp, not local time.
This is a regression test for the issue where created_at was using local time
but labeling it as UTC (with 'Z' suffix).
"""
# Use a specific Unix timestamp: 1733011890 = 2024-12-01T00:31:30Z (UTC)
utc_timestamp = 1733011890
mock_chunk = ChatCompletionChunk(
id="test_id",
choices=[ChunkChoice(index=0, delta=ChunkChoiceDelta(content="test", role="assistant"), finish_reason="stop")],
created=utc_timestamp,
model="test",
object="chat.completion.chunk",
)
client = OpenAIChatClient()
response_update = client._create_chat_response_update(mock_chunk)
# Verify that created_at is correctly formatted as UTC
assert response_update.created_at is not None
assert response_update.created_at.endswith("Z"), "Timestamp should end with 'Z' for UTC"
# Parse the timestamp and verify it matches UTC time
expected_utc_time = datetime.fromtimestamp(utc_timestamp, tz=timezone.utc)
expected_formatted = expected_utc_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
assert response_update.created_at == expected_formatted, (
f"Expected UTC timestamp {expected_formatted}, got {response_update.created_at}"
)
@@ -3,6 +3,7 @@
import asyncio
import base64
import os
from datetime import datetime, timezone
from typing import Annotated
from unittest.mock import MagicMock, patch
@@ -684,6 +685,51 @@ def test_create_response_content_with_mcp_approval_request() -> None:
assert req.function_call.additional_properties["server_label"] == "My_MCP"
def test_responses_client_created_at_uses_utc(openai_unit_test_env: dict[str, str]) -> None:
"""Test that ChatResponse from responses client uses UTC timestamp.
This is a regression test for the issue where created_at was using local time
but labeling it as UTC (with 'Z' suffix).
"""
client = OpenAIResponsesClient()
# Use a specific Unix timestamp: 1733011890 = 2024-12-01T00:31:30Z (UTC)
utc_timestamp = 1733011890
mock_response = MagicMock()
mock_response.output_parsed = None
mock_response.metadata = {}
mock_response.usage = None
mock_response.id = "test-id"
mock_response.model = "test-model"
mock_response.created_at = utc_timestamp
mock_message_content = MagicMock()
mock_message_content.type = "output_text"
mock_message_content.text = "Test response"
mock_message_content.annotations = None
mock_message_item = MagicMock()
mock_message_item.type = "message"
mock_message_item.content = [mock_message_content]
mock_response.output = [mock_message_item]
with patch.object(client, "_get_metadata_from_response", return_value={}):
response = client._create_response_content(mock_response, chat_options=ChatOptions()) # type: ignore
# Verify that created_at is correctly formatted as UTC
assert response.created_at is not None
assert response.created_at.endswith("Z"), "Timestamp should end with 'Z' for UTC"
# Parse the timestamp and verify it matches UTC time
expected_utc_time = datetime.fromtimestamp(utc_timestamp, tz=timezone.utc)
expected_formatted = expected_utc_time.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
assert response.created_at == expected_formatted, (
f"Expected UTC timestamp {expected_formatted}, got {response.created_at}"
)
def test_tools_to_response_tools_with_raw_image_generation() -> None:
"""Test that raw image_generation tool dict is handled correctly with parameter mapping."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")