Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/specs/004-python-function-calling-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,7 @@ that manually replay messages own the equivalent rule: do not resend an approval
| Pending placeholder history | An approval response remains replayable while its only result is `[APPROVAL_PENDING]`. | `packages/core/tests/core/test_sessions.py::test_filter_approval_controls_keeps_response_for_pending_placeholder` |
| Pending hosted history replay | Stateless hosted approval requests remain replayable until a response is recorded, then both controls become inert. | `packages/openai/tests/openai/test_openai_chat_client.py::test_stateless_history_preserves_pending_hosted_approval_request_until_response` |
| Non-history provider plus session | Local history is still auto-injected for approval resume. | `packages/core/tests/core/test_agents.py::test_non_history_context_provider_still_injects_inmemory` |
| Service-side approval decision | Stored request is skipped; current approved or rejected response is sent. | `packages/openai/tests/openai/test_openai_chat_client.py::test_prepare_messages_strips_approval_request_but_keeps_response_under_storage` |
| OpenAI approval serialization | Approval id and decision serialize to `mcp_approval_response`. | `test_prepare_message_for_openai_with_function_approval_response`, `test_prepare_content_for_opentool_approval_response`, `test_function_approval_response_with_mcp_tool_call` |
| OpenAI end-to-end hosted approval | Hosted request parses, response sends, and continuation completes. | `test_end_to_end_mcp_approval_flow` |
| Stored function call/result | Service-side storage drops server-issued calls but keeps new outputs. | `test_prepare_options_with_conversation_id_strips_server_issued_items`, `test_prepare_messages_for_openai_full_conversation_with_reasoning` |
Expand Down
5 changes: 5 additions & 0 deletions python/packages/openai/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ agent_framework_openai/

All clients follow the Raw + Full-Featured pattern (e.g., `RawOpenAIChatClient` + `OpenAIChatClient`).

For Responses API continuation with service-side storage, a prior `function_approval_request` is server-issued and
Comment thread
eavanvalkenburg marked this conversation as resolved.
must not be replayed inline, while the new `function_approval_response` is always serialized as
`mcp_approval_response` so the user's approved or rejected decision reaches the service. Applications that manually
replay message history must not send that same approval response again on later turns.

The generic OpenAI clients support both OpenAI and Azure OpenAI routing. Precedence is:
explicit Azure inputs (`credential`, `azure_endpoint`, `api_version`) → OpenAI API key
(`OPENAI_API_KEY`) → Azure environment fallback (`AZURE_OPENAI_*`).
Expand Down
10 changes: 9 additions & 1 deletion python/packages/openai/agent_framework_openai/_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1684,7 +1684,7 @@ def _prepare_message_for_openai(
)
if function_call:
all_messages.append(function_call)
case "function_approval_response" | "function_approval_request":
case "function_approval_request":
if request_uses_service_side_storage:
continue
prepared = self._prepare_content_for_openai(
Expand All @@ -1694,6 +1694,14 @@ def _prepare_message_for_openai(
)
if prepared:
all_messages.append(prepared)
case "function_approval_response":
prepared = self._prepare_content_for_openai(
message.role,
content,
replays_local_storage=replays_local_storage,
)
if prepared:
all_messages.append(prepared)
case "mcp_server_tool_call" | "mcp_server_tool_result":
# Hosted MCP call/result contents serialize as a single
# top-level mcp_call input item; the result side emits an
Expand Down
12 changes: 7 additions & 5 deletions python/packages/openai/tests/openai/test_openai_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7784,9 +7784,9 @@ def test_prepare_messages_keeps_function_call_without_storage() -> None:
assert output_item["call_id"] == "call_1"


def test_prepare_messages_strips_approval_items_under_storage() -> None:
"""Approval request/response items also carry server-issued IDs and must be stripped under
storage. Without storage they are kept (#3295)."""
@pytest.mark.parametrize("approved", [True, False], ids=["approved", "rejected"])
def test_prepare_messages_strips_approval_request_but_keeps_response_under_storage(approved: bool) -> None:
"""Stored requests are not replayed, but the new approval decision must reach the service."""
client = OpenAIChatClient(model="test-model", api_key="test-key")

function_call = Content.from_function_call(
Expand All @@ -7799,7 +7799,7 @@ def test_prepare_messages_strips_approval_items_under_storage() -> None:
function_call=function_call,
)
approval_response = Content.from_function_approval_response(
approved=True,
approved=approved,
id="approval_req_1",
function_call=function_call,
)
Expand All @@ -7811,7 +7811,9 @@ def test_prepare_messages_strips_approval_items_under_storage() -> None:
storage_on = client._prepare_messages_for_openai(messages, request_uses_service_side_storage=True)
storage_on_types = [item.get("type") for item in storage_on]
assert "mcp_approval_request" not in storage_on_types
assert "mcp_approval_response" not in storage_on_types
assert storage_on_types == ["mcp_approval_response"]
assert storage_on[0]["approval_request_id"] == "approval_req_1"
assert storage_on[0]["approve"] is approved

storage_off = client._prepare_messages_for_openai(messages, request_uses_service_side_storage=False)
storage_off_types = [item.get("type") for item in storage_off]
Expand Down
Loading