Skip to content
Closed
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
22 changes: 17 additions & 5 deletions src/agents/models/chatcmpl_stream_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,19 +339,30 @@ def _accumulate_tool_call_delta(
)

@staticmethod
def _buffered_tool_call_delta(
buffered_call: _BufferedToolCall,
) -> ChoiceDeltaToolCall:
if not buffered_call.call_id:
def _validate_completed_tool_call(call_id: str | None, name: str | None) -> None:
"""Reject a finalized tool call with a missing id or name.

Shared by the buffered path (`_buffered_tool_call_delta()`) and the unbuffered
fallback path in `handle_stream()`, so both fail with the same diagnostics instead
of the unbuffered path silently emitting a `call_id=""` function call.
"""
if not call_id:
raise ModelBehaviorError(
"Buffered Chat Completions tool call stream ended without a tool call id."
)

if not buffered_call.name:
if not name:
raise ModelBehaviorError(
"Buffered Chat Completions tool call stream ended without a function name."
)

@classmethod
def _buffered_tool_call_delta(
cls,
buffered_call: _BufferedToolCall,
) -> ChoiceDeltaToolCall:
cls._validate_completed_tool_call(buffered_call.call_id, buffered_call.name)

tool_call_delta = ChoiceDeltaToolCall(
index=buffered_call.index,
id=buffered_call.call_id,
Expand Down Expand Up @@ -1259,6 +1270,7 @@ async def handle_stream(
else:
# Function call was not streamed (fallback to old behavior)
# This handles edge cases where function name never arrived
cls._validate_completed_tool_call(function_call.call_id, function_call.name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve usage before rejecting the malformed call

When an unbuffered stream reports CompletionUsage and then ends with a missing call ID or function name, this new validation raises while the usage remains only in handle_stream()'s local variable. OpenAIChatCompletionsModel.stream_response() catches ModelBehaviorError and populates its generation span from the base response (openai_chatcompletions.py lines 508-512), which has not received that usage, so the failed request and its tokens disappear from tracing. Copy the built usage and raw-usage snapshot to response—or mark the request completed when usage is absent—before raising.

AGENTS.md reference: AGENTS.md:L167-L167

Useful? React with 👍 / 👎.

output_index = output_layout.function_call_output_index(state, index)
fallback_func_call_item = cls._function_call_item(
state,
Expand Down
Loading