Skip to content

[OpenAI Bridge] commentary phase is treated as TextDelta, causing duplicate assistant output in streaming responses #1979

@wimwinterberg

Description

@wimwinterberg

Describe Your Problem

When using the OpenAI Responses streaming API via the Symfony AI OpenAI bridge, assistant output can be duplicated.

This happens because messages with phase: "commentary" are currently handled the same way as phase: "final_answer". As a result, both are emitted as TextDelta, which leads to duplicated visible output.

Expected behavior

  • Only phase: "final_answer" should be emitted as TextDelta
  • phase: "commentary" should either:
    • be ignored, or
    • be mapped to ThinkingDelta (similar to reasoning_summary_text)

Actual behavior

Both commentary and final_answer phases are treated as regular output text and emitted as TextDelta, causing duplicated responses in the final output.


Steps to reproduce

  1. Use the OpenAI Responses API with streaming enabled
  2. Trigger a response where the model emits both:
    • phase: "commentary"
    • phase: "final_answer"
  3. Observe that the same content is appended twice in the final output

Technical details

In ResultConverter::convertStream():

if (str_contains($type, 'output_text') && isset($event['delta'])) {
    yield new TextDelta($event['delta']);
}

This logic does not differentiate between phases.

However, OpenAI emits response.output_text.delta for both:

  • commentary
  • final_answer

The distinction is only available via:

event.item_id → response.output_item → phase

Suggested solution

Track phase per item_id using response.output_item.added events:

$itemPhases[$event['item']['id']] = $event['item']['phase'] ?? null;

Then update handling of response.output_text.delta:

$phase = $itemPhases[$event['item_id']] ?? null;

if ($phase === 'final_answer') {
    yield new TextDelta($event['delta']);
}

if ($phase === 'commentary') {
    // optionally map to ThinkingDelta or ignore
}

Also consider replacing:

str_contains($type, 'output_text')

with a stricter check:

$type === 'response.output_text.delta'

References


Notes

  • reasoning_summary_text.* is already correctly handled by the bridge
  • commentary is different: it is emitted as a normal message with a phase attribute
  • This issue only affects streaming responses

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions