Python: Fix missing status input for OpenAI responses API (#4626)

* Fix missing status input for OpenAI responses API

* Fix mypy

* Address comments

* Remove raw_rep restore

* Do not set status if it's None
This commit is contained in:
Tao Chen
2026-03-11 14:20:23 -07:00
committed by GitHub
Unverified
parent 3e03a305f6
commit b1866bd279
3 changed files with 202 additions and 5 deletions
@@ -1019,6 +1019,7 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
content.type == "function_call"
and content.additional_properties
and "fc_id" in content.additional_properties
and content.additional_properties["fc_id"]
):
call_id_to_id[content.call_id] = content.additional_properties["fc_id"] # type: ignore[attr-defined, index]
list_of_list = [self._prepare_message_for_openai(message, call_id_to_id) for message in chat_messages]
@@ -1158,13 +1159,17 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
# OpenAI Responses API requires IDs to start with `fc_`
if not fc_id.startswith("fc_"):
fc_id = f"fc_{fc_id}"
return {
function_call_obj = {
"call_id": content.call_id,
"id": fc_id,
"type": "function_call",
"name": content.name,
"arguments": content.arguments,
}
if status := content.additional_properties.get("status"):
function_call_obj["status"] = status
return function_call_obj
case "function_result":
shell_output_type = (
content.additional_properties.get(OPENAI_SHELL_OUTPUT_TYPE_KEY)
@@ -1472,10 +1477,10 @@ class RawOpenAIResponsesClient( # type: ignore[misc]
case "function_call": # ResponseOutputFunctionCall
contents.append(
Content.from_function_call(
call_id=item.call_id if hasattr(item, "call_id") and item.call_id else "",
name=item.name if hasattr(item, "name") else "",
arguments=item.arguments if hasattr(item, "arguments") else "",
additional_properties={"fc_id": item.id} if hasattr(item, "id") else {},
call_id=item.call_id,
name=item.name,
arguments=item.arguments,
additional_properties={"fc_id": item.id, "status": item.status},
raw_representation=item,
)
)
@@ -602,3 +602,70 @@ async def test_integration_client_agent_existing_session():
assert isinstance(second_response, AgentResponse)
assert second_response.text is not None
assert "photography" in second_response.text.lower()
# region Integration with Foundry V2
skip_if_azure_ai_integration_tests_disabled = pytest.mark.skipif(
os.getenv("AZURE_AI_PROJECT_ENDPOINT", "") in ("", "https://test-project.cognitiveservices.azure.com/")
or os.getenv("AZURE_AI_MODEL_DEPLOYMENT_NAME", "") == "",
reason="No real AZURE_AI_PROJECT_ENDPOINT or AZURE_AI_MODEL_DEPLOYMENT_NAME provided; skipping integration tests.",
)
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_ai_integration_tests_disabled
async def test_integration_function_call_roundtrip_preserves_fidelity():
"""Test that function calls roundtrip correctly with full fidelity preserved.
This verifies the changes where:
1. raw_representation is preserved when parsing function calls
2. fc_id and status are included in additional_properties
3. When re-sending messages, the full object fidelity is preserved
"""
call_count = 0
@tool(name="get_weather", approval_mode="never_require")
async def get_weather_tool(location: str) -> str:
"""Get weather for a location."""
nonlocal call_count
call_count += 1
return f"Weather in {location} is sunny, 72F"
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
async with Agent(
client=client,
name="WeatherAgent",
instructions="You help check weather. Use get_weather when asked about weather.",
tools=[get_weather_tool],
default_options={"store": False}, # Store messages locally to test fidelity across messages
) as agent:
session = agent.create_session()
# First request - should invoke the tool
response1 = await agent.run("What is the weather in Seattle?", session=session)
assert response1 is not None
assert response1.text is not None
assert call_count >= 1
# Verify the response contains expected content
response_text = response1.text.lower()
assert "seattle" in response_text or "sunny" in response_text or "72" in response_text
# Second request - should work correctly with the preserved conversation
response2 = await agent.run("And how about in Portland?", session=session)
assert response2 is not None
assert response2.text is not None
assert call_count >= 2
# endregion
@@ -3363,3 +3363,128 @@ async def test_prepare_options_excludes_continuation_token() -> None:
# endregion
# region Function Call Fidelity Tests
def test_parse_response_from_openai_function_call_includes_status() -> None:
"""Test _parse_response_from_openai includes status in function call additional_properties."""
from openai.types.responses import ResponseFunctionToolCall
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Create a real ResponseFunctionToolCall object
mock_function_call_item = ResponseFunctionToolCall(
type="function_call",
call_id="call_123",
name="get_weather",
arguments='{"location": "Seattle"}',
id="fc_456",
status="completed",
)
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 = 1000000000
mock_response.output = [mock_function_call_item]
response = client._parse_response_from_openai(mock_response, options={}) # type: ignore
assert len(response.messages[0].contents) == 1
function_call = response.messages[0].contents[0]
assert function_call.type == "function_call"
assert function_call.call_id == "call_123"
assert function_call.name == "get_weather"
assert function_call.arguments == '{"location": "Seattle"}'
# Verify status is included in additional_properties
assert function_call.additional_properties is not None
assert function_call.additional_properties.get("status") == "completed"
assert function_call.additional_properties.get("fc_id") == "fc_456"
# Verify raw_representation is preserved
assert function_call.raw_representation is mock_function_call_item
def test_prepare_messages_for_openai_filters_empty_fc_id() -> None:
"""Test _prepare_messages_for_openai correctly filters empty fc_id values from call_id_to_id mapping."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
messages = [
Message(role="user", contents=[Content.from_text(text="check hotels")]),
Message(
role="assistant",
contents=[
# Function call with empty fc_id - should NOT be added to call_id_to_id
Content.from_function_call(
call_id="call_empty",
name="search_hotels",
arguments='{"city": "Paris"}',
additional_properties={"fc_id": ""}, # Empty string
),
],
),
Message(
role="assistant",
contents=[
# Function call with valid fc_id - SHOULD be added to call_id_to_id
Content.from_function_call(
call_id="call_valid",
name="search_flights",
arguments='{"from": "NYC"}',
additional_properties={"fc_id": "fc_valid123"},
),
],
),
]
result = client._prepare_messages_for_openai(messages)
# Find the function_call items in the result
fc_items = [item for item in result if item.get("type") == "function_call"]
assert len(fc_items) == 2
# The empty fc_id should result in an auto-generated id (starts with fc_)
empty_fc_item = next(item for item in fc_items if item.get("call_id") == "call_empty")
assert empty_fc_item["id"].startswith("fc_")
assert empty_fc_item["id"] != ""
# The valid fc_id should be preserved
valid_fc_item = next(item for item in fc_items if item.get("call_id") == "call_valid")
assert valid_fc_item["id"] == "fc_valid123"
def test_prepare_messages_for_openai_filters_none_fc_id() -> None:
"""Test _prepare_messages_for_openai correctly filters None fc_id values."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
messages = [
Message(
role="assistant",
contents=[
# Function call with None fc_id value
Content.from_function_call(
call_id="call_none",
name="get_info",
arguments="{}",
additional_properties={"fc_id": None}, # None value
),
],
),
]
result = client._prepare_messages_for_openai(messages)
# Find the function_call item
fc_items = [item for item in result if item.get("type") == "function_call"]
assert len(fc_items) == 1
# The None fc_id should result in an auto-generated id
fc_item = fc_items[0]
assert fc_item["id"].startswith("fc_")
# endregion