Skip to content
This repository was archived by the owner on Sep 23, 2026. It is now read-only.

fix(kosong): preserve empty-string reasoning_content as ThinkPart - #2498

Merged
RealKai42 merged 1 commit into
mainfrom
fix/kosong-empty-thinkpart
Jul 14, 2026
Merged

RealKai42 merged 1 commit into
mainfrom
fix/kosong-empty-thinkpart

Conversation

@bigeagle

@bigeagle bigeagle commented Jul 14, 2026 •

Copy link
Copy Markdown
Contributor

Related Issue

N/A — caught in a live session via request dump: coding-model-okapi-0711-vibe returned 400 preserved thinking (thinking.keep=all) requires reasoning_content on every assistant message, but assistant message at index 6 is missing reasoning_content.

Description

Empty-string reasoning_content means "reasoned but empty", not "no reasoning". The truthy checks in the Kimi stream/non-stream converters dropped empty deltas, so a turn whose stream carried reasoning_content: "" produced an assistant message with no ThinkPart at all. On the next request _convert_message then omitted reasoning_content for that turn, and preserved-thinking backends (some always-thinking models enforce thinking.keep=all by default) reject the request with 400.

Wire-log evidence from the incident session: the failing turn's stream contained only the ToolCall (output = 109 tokens ≈ the tool-call JSON), with no ContentPart at all — the model emitted empty reasoning for that turn.

Changes:

  • kimi.py: use is not None instead of truthy checks in both _convert_stream_response and _convert_non_stream_response, so empty-string reasoning_content yields ThinkPart(think=""), which _convert_message round-trips as reasoning_content: "" via the existing has_reasoning path.
  • A delta/response with the field genuinely absent still yields no ThinkPart — the empty-vs-absent distinction is preserved.
  • Tests: streaming empty delta, non-streaming empty field, and absent field (must not fabricate a ThinkPart).

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked the related issue, if any.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have run make gen-changelog to update the changelog.
  • I have run make gen-docs to update the user documentation.

Open in Devin Review

Empty-string reasoning_content means "reasoned but empty", not "no
reasoning" — but the truthy checks in the Kimi stream/non-stream
converters dropped empty deltas, conflating the two. The stored
assistant message then had no ThinkPart at all, so the next request
omitted reasoning_content for that turn; preserved-thinking backends
(thinking.keep=all, default-enforced on some models) reject such
requests with 400 "assistant message is missing reasoning_content".

Use 'is not None' in both converters so empty reasoning_content yields
ThinkPart(think=""), which _convert_message round-trips as
reasoning_content: "" via the existing has_reasoning path. Absent
field still yields no ThinkPart — the distinction is preserved.
Copilot AI review requested due to automatic review settings July 14, 2026 06:49

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes Kimi provider message conversion in packages/kosong to correctly preserve the semantic difference between an absent reasoning_content field and a present-but-empty reasoning_content: "", which is required for “preserved thinking” backends that enforce thinking.keep=all and expect reasoning_content on every assistant message.

Changes:

  • Update Kimi streaming and non-streaming response converters to treat reasoning_content="" as meaningful (emit ThinkPart(think="")) by switching from truthy checks to is not None.
  • Add regression tests covering: streaming empty delta, non-streaming empty field, and absent field (must not fabricate a ThinkPart).
  • Document the fix in packages/kosong/CHANGELOG.md.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
packages/kosong/src/kosong/chat_provider/kimi.py Preserves empty-string reasoning_content by emitting ThinkPart(think="") when the field is present (even if empty).
packages/kosong/tests/api_snapshot_tests/test_kimi.py Adds tests ensuring empty vs absent reasoning_content is preserved for both streaming and non-streaming paths.
packages/kosong/CHANGELOG.md Adds an Unreleased entry describing the preserved-thinking 400 fix.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@RealKai42
RealKai42 added this pull request to the merge queue Jul 14, 2026
Merged via the queue into main with commit 0b93ad3 Jul 14, 2026
22 checks passed
@RealKai42
RealKai42 deleted the fix/kosong-empty-thinkpart branch July 14, 2026 07:11
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants