fix: don't create empty output items from Responses events that never fill them

applyResponseStreamEvent calls ensureItem, which creates the output item when it is missing, before it knows whether any branch below actually needs one. Every branch that follows is type-guarded; ensureItem was not. So response.created on a fresh message pushed a synthetic empty message item into the assistant's output, and so did in_progress, queued, failed and incomplete, along with any other unhandled response.* event arriving with an item_id that is not in the list yet.

Nothing appeared on screen, since the renderer and getOutputText both skip items with no text, but the placeholder was saved into the chat, counted by estimateTokens, and replayed upstream: convert_to_responses_payload feeds stored output items back as input and _normalize_stored_item keeps type, role and content, so an empty assistant message was sent to the provider on every later turn of that conversation. The backend already carries a narrow workaround for this exact shape when continuing a message.

ensureItem is now reachable only by the event types the branches below handle, which mirrors the catch-all the backend reducer already has. Replaying a full stream gives the two real items and no placeholder, with rendered text identical before and after, and the handled events still produce identical state.
This commit is contained in:
Classic298 2026-08-14 20:43:25 +02:00
parent d02b6a21fc
commit 9e4437f081

View file

@ -459,6 +459,16 @@ export function applyResponseStreamEvent(
return nextOutput;
}
// ensureItem below creates the item when it is missing, so only types handled below may reach it.
const handledBelow =
eventType === 'response.content_part.added' ||
eventType === 'response.reasoning_summary_part.added' ||
eventType.endsWith('.delta') ||
eventType.endsWith('.done');
if (!handledBelow) {
return output;
}
const item = ensureItem(nextOutput, outputIndex, {
id: event.item_id,
type: eventType.includes('reasoning')