Skip to content

fix: skip phantom empty AI message from Responses API streaming#658

Open
cristipufu wants to merge 1 commit intomainfrom
fix/phantom-empty-message-responses-api
Open

fix: skip phantom empty AI message from Responses API streaming#658
cristipufu wants to merge 1 commit intomainfrom
fix/phantom-empty-message-responses-api

Conversation

@cristipufu
Copy link
Member

Summary

  • Fixes duplicate empty "green dot" AI message appearing in the chat UI when using UiPathChatOpenAI with use_responses_api=True
  • Root cause: OpenAI Responses API response.created SSE event produces an AIMessageChunk with id=resp_xxx and empty content, while all subsequent chunks get id=None (reassigned to run-xxx by LangChain). The mapper treats these as two separate messages — one phantom, one real.
  • Adds a guard in map_ai_message_chunk_to_events() that silently drops metadata-only chunks (no content, content_blocks, tool_call_chunks, or tool_calls) that aren't the final chunk

Root cause details

In langchain_openai, _convert_responses_chunk_to_generation_chunk() initializes id = None for every chunk but sets id = chunk.response.id only for response.created. LangChain's BaseChatModel then assigns run_id to all chunks with id=None. This creates two different IDs in the stream:

Event chunk ID content
response.created resp_abc123 empty
response.output_text.delta run-xxx (was None) text
response.completed run-xxx (was None) chunk_position="last"

The upstream bug exists in langchain-openai (related: langchain#34138, closed without merge). This PR adds a defensive guard in the mapper layer.

Test plan

  • Added test_map_event_skips_empty_metadata_chunk_with_new_id — verifies phantom chunks are dropped
  • Added test_map_event_responses_api_no_phantom_message — end-to-end Responses API simulation
  • Updated existing tests to use realistic chunk content
  • All 69 tests pass

🤖 Generated with Claude Code

When using the OpenAI Responses API (`use_responses_api=True`), the
`response.created` SSE event produces an AIMessageChunk with a unique
`id` (e.g. `resp_xxx`) but no content. All subsequent chunks (text
deltas, tool calls, response.completed) arrive with `id=None`, which
LangChain's BaseChatModel reassigns to a callback `run_id`. This ID
mismatch causes the mapper to emit a phantom empty `message_start`
for the `resp_` chunk that never receives content or an end event,
resulting in a duplicate empty "green dot" AI message in the chat UI.

The fix adds a guard in `map_ai_message_chunk_to_events()` that skips
registering a new message for chunks that carry no payload (no content,
content_blocks, tool_call_chunks, or tool_calls) and are not the final
chunk. This silently drops the metadata-only `response.created` chunk
while preserving all other streaming behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant