Description
When using a custom OpenAI-compatible provider (chat completions), streamed assistant replies render in the TUI with one token per line, e.g.:
Root cause
Some gateways include both content and reasoning_content in every stream chunk, with the inactive one set to an empty string:
{"choices":[{"delta":{"content":"固件","reasoning_content":""}}]}
In packages/kosong, the OpenAI chat-completions provider (openai-legacy) yields a think part whenever the reasoning field is a string — including "". The part stream therefore alternates think("") → text → think("") → text → …. Downstream, mergeInPlace only merges adjacent same-type parts, so the text deltas can no longer merge and the assembled message ends up with one text part per token. The TUI renders each text part as its own block → a line break per token.
Providers whose gateway omits the reasoning field entirely during the content phase (e.g. DeepSeek's official API) are unaffected.
Reproduction
- Configure an
openai-type provider whose gateway pads "reasoning_content":"" into every chunk (observed with GLM-class models behind an OpenAI-compatible subscription gateway).
- Ask anything and watch the streamed reply: every token lands on its own line.
Expected behavior
Empty reasoning strings are ignored, text deltas stay adjacent, and the reply renders as continuous text.
Fix
I have a ready patch: skip empty reasoning strings at both yield sites (stream and non-stream) in the provider, with regression tests. PR incoming.
Description
When using a custom OpenAI-compatible provider (chat completions), streamed assistant replies render in the TUI with one token per line, e.g.:
Root cause
Some gateways include both
contentandreasoning_contentin every stream chunk, with the inactive one set to an empty string:{"choices":[{"delta":{"content":"固件","reasoning_content":""}}]}In
packages/kosong, the OpenAI chat-completions provider (openai-legacy) yields athinkpart whenever the reasoning field is a string — including"". The part stream therefore alternatesthink("") → text → think("") → text → …. Downstream,mergeInPlaceonly merges adjacent same-type parts, so the text deltas can no longer merge and the assembled message ends up with one text part per token. The TUI renders each text part as its own block → a line break per token.Providers whose gateway omits the reasoning field entirely during the content phase (e.g. DeepSeek's official API) are unaffected.
Reproduction
openai-type provider whose gateway pads"reasoning_content":""into every chunk (observed with GLM-class models behind an OpenAI-compatible subscription gateway).Expected behavior
Empty reasoning strings are ignored, text deltas stay adjacent, and the reply renders as continuous text.
Fix
I have a ready patch: skip empty reasoning strings at both yield sites (stream and non-stream) in the provider, with regression tests. PR incoming.