fix(kosong): repair inline newlines from Alibaba gateway streams - #2375
fix(kosong): repair inline newlines from Alibaba gateway streams#2375godLei6 wants to merge 1 commit into
Conversation
Some models (e.g. Qwen) served through Alibaba token-plan / dashscope gateways emit spurious newlines inside Chinese text at phrase boundaries. These break Markdown list-item lazy continuation and split table rows, so the TUI renders lists and tables as broken plain text. Detect Alibaba gateways by baseUrl and, only for those requests, buffer streamed text deltas and replace non-structural newlines with spaces while preserving real structural breaks (blank lines, headings, list markers, blockquotes, table pipes, code fences, horizontal rules). Non-streaming responses are repaired with the same rules. Other providers are unaffected. Adds unit tests for the helper predicates and full-text repair, plus streaming provider-integration tests covering both Alibaba and non-Alibaba base URLs.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f1222a03e3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const nlIdx = this._nlBuffer.indexOf('\n'); | ||
| if (nlIdx === -1) break; |
There was a problem hiding this comment.
Keep Alibaba streams incremental without newlines
For Alibaba streams, a normal text delta that contains no newline only grows _nlBuffer and then exits this loop with output === ''; the streaming path only yields when _processNlRepair returns text, so a long single-line paragraph is hidden until a later newline or the final flush. This regresses the streaming UX for common responses where tokens arrive for seconds without a newline; keep only the newline boundary pending and continue yielding safe text as deltas arrive.
Useful? React with 👍 / 👎.
| const before = this._nlBuffer.slice(0, nlIdx); | ||
| const after = this._nlBuffer.slice(nlIdx + 1); |
There was a problem hiding this comment.
Preserve blank lines in streamed newline repair
When an Alibaba stream contains a paragraph break such as para1\n\npara2, after is the entire remaining buffer (\npara2) rather than the next line. That makes isStructuralNewline miss the empty line and replace the first newline with a space, producing para1 \npara2; the non-stream repair path preserves the same blank line, so streamed Markdown paragraphs are corrupted.
Useful? React with 👍 / 👎.
| if (/^(`{3,}|~{3,})/.test(after.trim())) { | ||
| output += before + '\n'; | ||
| this._nlInCodeBlock = true; | ||
| this._nlBuffer = after; |
There was a problem hiding this comment.
Don't enter code-block state before consuming the fence
When ordinary text precedes an untyped fenced code block in the same buffered Alibaba stream, this branch sets _nlInCodeBlock while the opening fence line is still in _nlBuffer; on the next loop that opening fence is processed by the in-code-block branch and is mistaken for the closing fence, so subsequent code lines are merged by the repair heuristic. This corrupts common fenced code blocks that start after prose.
Useful? React with 👍 / 👎.
Problem
When models such as Qwen are served through Alibaba's token-plan / dashscope
gateways (
*.aliyuncs.com), the streamed text deltas contain spuriousnewlines inserted at Chinese phrase boundaries. These newlines are not
structural Markdown, but the renderer (marked) treats them as block
boundaries, which breaks:
render as separate plain paragraphs.
as a row, so the table collapses into raw pipe-delimited text.
The same output renders correctly with Kimi's own models, which do not emit
these inline newlines, so the issue is specific to the gateway's streaming
behavior rather than the TUI renderer.
Fix
Detect Alibaba gateways by
baseUrl(matchingaliyuncs.com/dashscope)in the OpenAI-compatible legacy provider, and only for those requests
repair the text stream:
is held until the next chunk arrives so its structural context can be
judged.
"soft line break = space" semantics, extended to list lazy-continuation and
table rows).
blockquotes, table pipes, code fences, and horizontal rules — plus lines
immediately following a heading or a code fence.
All other providers (Kimi, OpenAI, Anthropic, DeepSeek, etc.) are completely
unaffected — the repair path is gated on the gateway hostname and adds no
overhead elsewhere. No config change is required from the user.
Tests
New
packages/kosong/test/openai-legacy-newline-repair.test.tscovers:isAlibabaGatewayhostname matching (positive + negative cases).isStructuralNewlinefor every structural marker and the inline-continuationnegative case.
repairFullTextfor list continuation, broken table rows, and preservationof normal lists / paragraphs / headings / fenced code blocks / ordered lists.
baseUrlmerges a broken listitem across deltas, a non-Alibaba
baseUrlleaves the newlines intact, andstructural newlines survive the Alibaba repair path.
The full
kosongsuite (1383 tests) andtsc --noEmitboth pass.Scope
packages/kosong/src/providers/openai-legacy.ts(plus the new test file).