Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Only write entries that are worth mentioning to users.
- Core: Add `skip_yolo_prompt_injection` config option to suppress the system reminder normally injected when yolo mode is active — useful when building custom applications on top of `KimiSoul` that do not need the non-interactive mode hint
- Kimi: Add `KIMI_MODEL_THINKING_KEEP` environment variable that forwards its value verbatim to the Moonshot API as `thinking.keep`, enabling Preserved Thinking (e.g. `export KIMI_MODEL_THINKING_KEEP=all` to retain historical `reasoning_content` across turns); effective only for Moonshot models supporting Preserved Thinking (e.g. `kimi-k2.6` / `kimi-k2-thinking`), unset or empty string preserves the previous behavior and omits the field, and the override only applies when the current model is actually in thinking mode so the API never receives a `thinking.keep` without the companion `thinking.type`. Note that `keep=all` increases input tokens and API cost because history reasoning is resent
- Kosong: Fix `Kimi.with_extra_body` silently dropping previously set `thinking.type` when a later call added another `thinking.*` field — the `thinking` sub-dict is now merged field-by-field instead of shallow-replaced, so composing `with_thinking(...)` with `with_extra_body({"thinking": {...}})` preserves both contributions
- Kosong: Fix Kimi provider sending empty `content` alongside `tool_calls`, which caused 400 "text content is empty" errors from the Moonshot API. When an assistant message has tool calls and its visible content is effectively empty (no text or only whitespace/think parts), the `content` field is now omitted entirely
- Shell: Fix approval request feedback text cursor rendering — the block cursor now correctly renders at the actual cursor position instead of always being pinned to the end of the line; when the cursor is in the middle of the text, the character under the cursor is drawn with reverse video (mimicking a terminal's native block cursor)

## 1.38.0 (2026-04-22)
Expand Down
1 change: 1 addition & 0 deletions docs/en/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This page documents the changes in each Kimi Code CLI release.
- Core: Add `skip_yolo_prompt_injection` config option to suppress the system reminder normally injected when yolo mode is active — useful when building custom applications on top of `KimiSoul` that do not need the non-interactive mode hint
- Kimi: Add `KIMI_MODEL_THINKING_KEEP` environment variable that forwards its value verbatim to the Moonshot API as `thinking.keep`, enabling Preserved Thinking (e.g. `export KIMI_MODEL_THINKING_KEEP=all` to retain historical `reasoning_content` across turns); effective only for Moonshot models supporting Preserved Thinking (e.g. `kimi-k2.6` / `kimi-k2-thinking`), unset or empty string preserves the previous behavior and omits the field, and the override only applies when the current model is actually in thinking mode so the API never receives a `thinking.keep` without the companion `thinking.type`. Note that `keep=all` increases input tokens and API cost because history reasoning is resent
- Kosong: Fix `Kimi.with_extra_body` silently dropping previously set `thinking.type` when a later call added another `thinking.*` field — the `thinking` sub-dict is now merged field-by-field instead of shallow-replaced, so composing `with_thinking(...)` with `with_extra_body({"thinking": {...}})` preserves both contributions
- Kosong: Fix Kimi provider sending empty `content` alongside `tool_calls`, which caused 400 "text content is empty" errors from the Moonshot API. When an assistant message has tool calls and its visible content is effectively empty (no text or only whitespace/think parts), the `content` field is now omitted entirely
- Shell: Fix approval request feedback text cursor rendering — the block cursor now correctly renders at the actual cursor position instead of always being pinned to the end of the line; when the cursor is in the middle of the text, the character under the cursor is drawn with reverse video (mimicking a terminal's native block cursor)

## 1.38.0 (2026-04-22)
Expand Down
1 change: 1 addition & 0 deletions docs/zh/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Core:新增 `skip_yolo_prompt_injection` 配置项,用于抑制 yolo 模式下注入的系统提示词——基于 `KimiSoul` 构建自定义应用且不需要该提示时很有用
- Kimi:新增环境变量 `KIMI_MODEL_THINKING_KEEP`,将其值原样作为 `thinking.keep` 字段发送给 Moonshot API,用于启用 Preserved Thinking(例如 `export KIMI_MODEL_THINKING_KEEP=all` 可让模型在多轮之间保留历史 `reasoning_content`);仅对支持 Preserved Thinking 的 Moonshot 模型(如 `kimi-k2.6` / `kimi-k2-thinking`)生效,未设置或空字符串时请求体不携带该字段、等同当前默认行为,且仅在当前模型真正处于 Thinking 模式时才注入,以避免 API 收到只有 `thinking.keep` 而缺少 `thinking.type` 的无效请求体。注意 `keep=all` 会因为重新发送历史推理内容而显著增加输入 token 与 API 费用
- Kosong:修复 `Kimi.with_extra_body` 在后续调用新增其它 `thinking.*` 字段时静默丢掉已有 `thinking.type` 的问题——`thinking` 子对象现在按字段合并,而不是被整体浅覆盖,使得 `with_thinking(...)` 与 `with_extra_body({"thinking": {...}})` 组合使用时两次设置的字段都能保留
- Kosong:修复 Kimi provider 在 `tool_calls` 旁发送空 `content` 导致 Moonshot API 返回 400 "text content is empty" 错误的问题。当 Assistant 消息带有工具调用且可见内容实际为空(无文本或仅包含空白 / think 部分)时,现在会完全省略 `content` 字段
- Shell:修复审批请求反馈文本输入的光标渲染问题——光标块现在根据实际光标位置正确渲染,不再始终固定在行尾;当光标位于文本中间时,光标所在字符会以反色显示(模拟终端原生块光标效果)

## 1.38.0 (2026-04-22)
Expand Down
1 change: 1 addition & 0 deletions packages/kosong/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Kimi: Add `keep` to `ThinkingConfig` (Moonshot `thinking.keep` passthrough for Preserved Thinking); value is typed as `Any` and forwarded unchanged, with case-preservation and no validation — callers choose a value the server accepts (e.g. `"all"`)
- Kimi: Fix `with_extra_body` silently dropping earlier `thinking.*` fields on subsequent calls — the `thinking` sub-dict is now merged field-by-field so composing `with_thinking(...)` with `with_extra_body({"thinking": {...}})` preserves both contributions; other top-level keys retain last-writer-wins semantics
- Kimi: Fix sending empty `content` alongside `tool_calls`, which caused 400 "text content is empty" errors from the Moonshot API. When an assistant message has tool calls and its visible content is effectively empty (no text or only whitespace/think parts), the `content` field is now omitted entirely

## 0.51.0 (2026-04-22)

Expand Down
21 changes: 21 additions & 0 deletions packages/kosong/src/kosong/chat_provider/kimi.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,11 +310,32 @@ def _convert_message(message: Message) -> ChatCompletionMessageParam:
content.append(part)
message.content = content
dumped_message = message.model_dump(exclude_none=True)
if (
message.role == "assistant"
and message.tool_calls
and _is_effectively_empty_content_parts(content)
):
# OpenAI-compatible APIs allow assistant tool-call messages to omit
# `content`, but the Kimi-for-Coding compat layer rejects a content
# list that contains an empty text part (observed: `content:
# [{"type": "text", "text": ""}]` -> 400 "text content is empty").
# Dropping `content` entirely is always accepted, so do that whenever
# the visible content is effectively empty alongside a tool call.
dumped_message.pop("content", None)
if reasoning_content:
dumped_message["reasoning_content"] = reasoning_content
return cast(ChatCompletionMessageParam, dumped_message)


def _is_effectively_empty_content_parts(content: Sequence[ContentPart]) -> bool:
for part in content:
if not isinstance(part, TextPart):
return False
if part.text.strip():
return False
return True


def _convert_tool(tool: Tool) -> ChatCompletionToolParam:
if tool.name.startswith("$"):
# Kimi builtin functions start with `$`
Expand Down
69 changes: 68 additions & 1 deletion packages/kosong/tests/api_snapshot_tests/test_kimi.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from inline_snapshot import snapshot

from kosong.chat_provider.kimi import Kimi
from kosong.message import Message, TextPart, ThinkPart
from kosong.message import Message, TextPart, ThinkPart, ToolCall
from kosong.tooling import Tool

BUILTIN_TOOL = Tool(
Expand Down Expand Up @@ -36,6 +36,38 @@
Message(role="user", content="Thanks!"),
],
},
"assistant_tool_call_without_text": {
"history": [
Message(role="user", content="Call the add tool"),
Message(
role="assistant",
content=[],
tool_calls=[
ToolCall(
id="call_abc123",
function=ToolCall.FunctionBody(name="add", arguments='{"a": 2, "b": 3}'),
)
],
),
Message(role="tool", content="5", tool_call_id="call_abc123"),
],
},
"assistant_tool_call_with_reasoning_only": {
"history": [
Message(role="user", content="Think and call the add tool"),
Message(
role="assistant",
content=[ThinkPart(think="I should call the tool.")],
tool_calls=[
ToolCall(
id="call_abc123",
function=ToolCall.FunctionBody(name="add", arguments='{"a": 2, "b": 3}'),
)
],
),
Message(role="tool", content="5", tool_call_id="call_abc123"),
Comment on lines +39 to +68

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

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

The snapshot coverage added here exercises content=[] and content=[ThinkPart(...)], but the reported failure mode is an assistant tool-call message stored as a text part with empty/whitespace text (e.g. content=[TextPart(text="")], which serializes to content: "" or [{"type":"text","text":""}]). Consider adding an explicit snapshot case for empty/whitespace-only TextPart alongside tool_calls to ensure _is_effectively_empty_content_parts is actually covered for the original trigger.

Copilot uses AI. Check for mistakes.
],
},
}


Expand Down Expand Up @@ -285,6 +317,41 @@ async def test_kimi_message_conversion():
],
"tools": [],
},
"assistant_tool_call_without_text": {
"messages": [
{"role": "user", "content": "Call the add tool"},
{
"role": "assistant",
"tool_calls": [
{
"type": "function",
"id": "call_abc123",
"function": {"name": "add", "arguments": '{"a": 2, "b": 3}'},
}
],
},
{"role": "tool", "content": "5", "tool_call_id": "call_abc123"},
],
"tools": [],
},
"assistant_tool_call_with_reasoning_only": {
"messages": [
{"role": "user", "content": "Think and call the add tool"},
{
"role": "assistant",
"reasoning_content": "I should call the tool.",
"tool_calls": [
{
"type": "function",
"id": "call_abc123",
"function": {"name": "add", "arguments": '{"a": 2, "b": 3}'},
}
],
},
{"role": "tool", "content": "5", "tool_call_id": "call_abc123"},
],
"tools": [],
},
}
)

Expand Down
Loading
Loading