Repository navigation
Conversation
|
Thanks for the tip, although I am not yet able to reproduce the issue you are speaking of. Could you provide more details regarding your setup and reproducible steps? And does the issue still occur with the app on the |
|
Hi @edward-ly, I have not actively tested, but I expect it's still present on My setup:
With my setup a simple Assistant request such as To be clear, it's Mistral specific. I think they changed something when Mistral Large 4 went out of preview, because I had the same problem in a script I created once (which also uses Mistral). |
|
Ah right, I was using the |
|
@edward-ly thanks for your review! Team me + AI agent addressed both. One remark from my AI agent which I'm just going to copy-paste, because it's no use to rewrite this in my own words:
One behavior change after a review pass: the shared function skips a chunk that is not an array. The old loop read Also added tests. |
Please rewrite the AI output in your own words anyway before I re-review, as it is our AI policy. This is just to make sure that you understand what it is saying before submitting its work to us. |
I'm aware of the policy, but rewriting it just felt as re-ordering the words from the AI agent. I understand your reasoning, so in my own words: Monsieur Claude Code and I did not want to create a circular dependency by moving the helper to OpenAiAPIService, because then OpenAiAPIService calls StreamingService, which in turn calls the helper in OpenAiAPIService. Instead, I kept the helper on StreamingService and made it public, so createChatCompletion() now calls it. |
I appreciate your understanding, and I see what you're trying to say. How about moving the helper into its own utility class and making it Also, before pushing again, please rebase the branch rather than add any excessive merge commits. Also also, FYI I'll be taking a long weekend off from today until Tuesday, but I'll ask someone else on the team to finish the review in the meantime so that you hopefully won't have to wait too much longer. |
Mistral streams content as a list of typed chunks. The SSE parser only accepted string content, so answer text was dropped silently. Assisted-by: ClaudeCode:deepseek-v4.1-flash Signed-off-by: Dick Tump <dick@tump.me>
Check that a streamed content array is not empty before parsing it, and reuse the same chunk-to-text extraction in createChatCompletion() instead of a duplicate loop. Assisted-by: ClaudeCode:deepseek-v4.1-flash Signed-off-by: Dick Tump <dick@tump.me>
Move contentChunksToText() out of StreamingService so the streamed and non-streamed paths do not depend on each other for it. Both now call ContentChunkParser::contentChunksToText(). Add a direct unit test for the parser. Assisted-by: ClaudeCode:deepseek-v4.1-flash Signed-off-by: Dick Tump <dick@tump.me>
c9ada72 to
865b6ee
Compare
I think that's a good solution and more durable, so I moved the helper into its own utility class and made it static. And I rebased the branch so there's no merge commit. I was a bit late myself (busy at work), so no hurry. Enjoy your weekend off (please apply the 'enjoy' retroactively if you read it when you're back 😄) |
Summary
The stream parser reads
delta.contentonly when it is a string. Mistral sendscontentas a list of typed chunks. Each chunk has a type, for exampletext,thinking, orreference. The parser skips the list, so the answer text is lost. The task completes without an error and the Assistant shows no text.Upstream PR #409 fixed the same problem for non-streamed responses. Version 5.0.0 added the streamed path, and that path still has the problem.
How to reproduce it
Use the Mistral API at
https://api.eu.mistral.ai/v1with the modelmistral-large-4-0. Start a streamedcore:text2text:chatwithtoolstask with Context Agent tools. Setreasoning_efforttolow,medium, orhigh.The server sends each chunk with
"content":[{"type":"text","text":"..."}]. Some chunks also havethinkingparts. The response has status 200 andfinish_reason: stop. The assistant message is empty.Change
The stream parser now accepts an array for
delta.content. It appends only thetextparts, and it skipsthinkingandreferenceparts. An empty array does not hide the message content. This is the same behavior as the non-streamed path. Streamed reasoning text stays a separate change.The non-streamed path in
createChatCompletion()had its own copy of the chunk-to-text loop. The helper now lives inlib/Service/ContentChunkParser.phpas a static method, so the streamed and non-streamed paths call the same code.Test results
I added these tests to
tests/unit/Service/StreamingServiceTest.php:testTypedContentChunksAreAssembledAndStreamed:thinkingparts before thetextparts, with one-chunk and multi-chunk deltas. It checks the streamed fragments and the final choice content.testStringDeltasContinueAfterATypedContentChunk: a typed first chunk followed by string chunks.testEmptyContentArrayFallsBackToMessageContent: an empty array must not hide the message content.I also added
tests/unit/Service/ContentChunkParserTest.php. It checks that text chunks are concatenated, thatthinking,referenceand malformed chunks are skipped, and that an empty list returns an empty string.All 10 tests in the two files pass.
php -landphp-cs-fixerpass on the changed files, and psalm reports no issues in them.I tested the change on a live instance. The instance runs Nextcloud 35.0.2 with integration_openai 5.0.0 and the model
mistral-large-4-0through Context Agent. Before the change, the same Assistant request returned no text. After the change, the Assistant returns the answer and it creates the calendar event that the request asks for.Related
#408 and #409 cover the same problem for non-streamed responses.
AI assistance
I used an AI assistant (Claude Code / DeepSeek V4.1 Flash) to analyze the response format and the parser, and to help review the patch. I ran the live test myself.