LLM text chunks are streamed as message_file when assistant message has files
- Dominant language
- TypeScript
- Stars
- 156k
- Forks
- 24.6k
- Avg merge
- 22h 9m
- Merged PRs (30d)
- 610
Description
### Self Checks
- [x] I have read the [Contributing Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) and [Language Policy](https://github.com/langgenius/dify/issues/1542).
- [x] This is only for bug report, if you would like to ask a question, please head to [Discussions](https://github.com/langgenius/dify/discussions/categories/general).
- [x] I have searched for existing issues [search for existing issues](https://github.com/langgenius/dify/issues), including closed ones.
- [x] I confirm that I am using English to submit this report, otherwise it will be closed.
- [x] 【中文用户 & Non English User】请使用英语提交,否则会被关闭 :)
- [x] Please do not modify this template :) and fill in all the required fields.
### Dify version
`main` branch at `b96ea94505e3abe00daba69faa8bba9bcf9deb0a`.
### Cloud or Self Hosted
Self Hosted (Source) / upstream `main` code path.
### Steps to reproduce
1. Use a chat/completion flow that streams an assistant response and also produces an assistant-side message file for the same message. For example, a workflow/tool path that emits `QueueMessageFileEvent` before or during LLM token streaming.
2. The file event is handled by `MessageCycleManager.message_file_to_stream_response()`, which caches the message id in `_message_has_file` and correctly emits a `message_file` event for the file metadata.
3. When a later `QueueLLMChunkEvent` is processed in `EasyUIBasedGenerateTaskPipeline._process_stream_response()`, the code calls `MessageCycleManager.get_message_event_type(message_id=...)` and reuses that event type for LLM text chunks.
4. Since the same message already has an assistant file, `get_message_event_type()` returns `StreamEvent.MESSAGE_FILE`.
5. The LLM text chunk is therefore emitted through `message_to_stream_response(..., event_type=StreamEvent.MESSAGE_FILE)` instead of `StreamEvent.MESSAGE`.
Current code evidence on `main`:
- `api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py` still precomputes the event type for `QueueLLMChunkEvent` by calling `get_message_event_type(message_id=self._message_id)` and passes it to `message_to_stream_response()`.
- `api/core/app/task_pipeline/message_cycle_manager.py` returns `StreamEvent.MESSAGE_FILE` from `get_message_event_type()` when the message id is present in `_message_has_file` or when an assistant `MessageFile` exists for that message.
Related previously closed PR with a small fix and unit test: https://github.com/langgenius/dify/pull/31414
### ✔️ Expected Behavior
`QueueLLMChunkEvent` text chunks should always be streamed as `event: message`, regardless of whether the same assistant message also has files.
File metadata should continue to be streamed separately as `event: message_file` via `QueueMessageFileEvent`.
### ❌ Actual Behavior
When the assistant message has a file, LLM text chunks can be streamed as `event: message_file`. The web client may treat those chunks as file events instead of normal message text, so streamed answer text can be dropped or not rendered correctly.
Contributor guide
Assessment
This issue has not been assessed yet.