Skip to content

fix(kosong): repair inline newlines from Alibaba gateway streams - #2375

Open
godLei6 wants to merge 1 commit into
MoonshotAI:mainfrom
godLei6:fix/alibaba-gateway-newline-repair
Open

fix(kosong): repair inline newlines from Alibaba gateway streams#2375
godLei6 wants to merge 1 commit into
MoonshotAI:mainfrom
godLei6:fix/alibaba-gateway-newline-repair

Conversation

@godLei6

@godLei6 godLei6 commented Jul 29, 2026

Copy link
Copy Markdown

Problem

When models such as Qwen are served through Alibaba's token-plan / dashscope
gateways (*.aliyuncs.com), the streamed text deltas contain spurious
newlines inserted at Chinese phrase boundaries. These newlines are not
structural Markdown, but the renderer (marked) treats them as block
boundaries, which breaks:

  • List items — lazy continuation lines lose their indentation context and
    render as separate plain paragraphs.
  • Tables — a single table row split across two lines is no longer parsed
    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 (matching aliyuncs.com / dashscope)
in the OpenAI-compatible legacy provider, and only for those requests
repair the text stream:

  • Streamed deltas are line-buffered; a newline sitting at the end of a chunk
    is held until the next chunk arrives so its structural context can be
    judged.
  • Non-structural newlines are replaced with a space (matching the CommonMark
    "soft line break = space" semantics, extended to list lazy-continuation and
    table rows).
  • Structural newlines are preserved: blank lines, headings, list markers,
    blockquotes, table pipes, code fences, and horizontal rules — plus lines
    immediately following a heading or a code fence.
  • Code-block interiors are passed through untouched.
  • Non-streaming responses are repaired with the same rules in one pass.

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.ts covers:

  • isAlibabaGateway hostname matching (positive + negative cases).
  • isStructuralNewline for every structural marker and the inline-continuation
    negative case.
  • repairFullText for list continuation, broken table rows, and preservation
    of normal lists / paragraphs / headings / fenced code blocks / ordered lists.
  • Streaming provider integration: an Alibaba baseUrl merges a broken list
    item across deltas, a non-Alibaba baseUrl leaves the newlines intact, and
    structural newlines survive the Alibaba repair path.

The full kosong suite (1383 tests) and tsc --noEmit both pass.

Scope

  • Single source file changed: packages/kosong/src/providers/openai-legacy.ts
    (plus the new test file).
  • No changes to the renderer, agent-core, TUI layer, or config schema.

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.
@changeset-bot

changeset-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: f1222a0

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +421 to +422
const nlIdx = this._nlBuffer.indexOf('\n');
if (nlIdx === -1) break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +423 to +424
const before = this._nlBuffer.slice(0, nlIdx);
const after = this._nlBuffer.slice(nlIdx + 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +442 to +445
if (/^(`{3,}|~{3,})/.test(after.trim())) {
output += before + '\n';
this._nlInCodeBlock = true;
this._nlBuffer = after;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

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