Preserve incomplete response output in failed inference traces (#58456)

## Why

When a response fails with incomplete output, attempted tool calls can appear only in the terminal response payload. Failed inference traces previously omitted those items, losing evidence of the attempted call.

## What changed

Retain output items marked `incomplete` on `ResponseIncompleteError` and include recognized items in failed inference traces, deduplicating by item ID. Keep these items out of streamed output, tool dispatch, and the continuation cache, and omit their payloads from the error's debug representation.

## Testing

Add tests for retaining only incomplete output from terminal responses and recording attempted calls in failed traces without streaming them or creating tool calls. Cover unknown response item types being skipped.

GitOrigin-RevId: 594f6a37a5af543694f19fcffe0c164cb2addb36
This commit is contained in:
cassirer-openai
2026-10-30 23:55:47 +00:00
committed by copyberry
parent 26cc286314
commit 7656a23b8d
6 changed files with 114 additions and 1 deletions
+28
View File
@@ -475,6 +475,15 @@ pub fn process_responses_event(
.map(Into::into);
return Err(ResponsesEventError::Api(ApiError::ResponseIncomplete(
Box::new(ResponseIncompleteError {
partial_output: response
.and_then(|r| r.get("output")?.as_array())
.into_iter()
.flatten()
.filter(|item| {
item.get("status").and_then(Value::as_str) == Some("incomplete")
})
.cloned()
.collect(),
reason: reason.to_string(),
response_id,
token_usage,
@@ -1407,6 +1416,25 @@ mod tests {
}
}
#[tokio::test]
async fn incomplete_response_retains_partial_output_on_the_error() {
let call = json!({
"type": "function_call", "status": "incomplete", "id": "fc_attempt",
"call_id": "attempt", "name": "unknown", "arguments": "{}",
});
let terminal = json!({
"type": "response.incomplete",
"response": {
"id": "resp_1", "incomplete_details": {"reason": "invalid_recipient"},
"output": [{"type": "message", "status": "completed"}, call],
}
});
let sse = format!("event: response.incomplete\ndata: {terminal}\n\n");
let events = collect_events(&[sse.as_bytes()]).await;
assert_matches!(events.as_slice(), [Err(ApiError::ResponseIncomplete(error))]
if error.reason == "invalid_recipient" && error.partial_output == [call]);
}
#[tokio::test]
async fn typed_errors_handle_missing_or_blank_message() {
for (code, fallback) in [