Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -12,6 +12,7 @@ Only write entries that are worth mentioning to users.
## Unreleased

- Kosong: Stop sending an empty `anthropic-beta` header when no beta features are declared — adaptive thinking removes the interleaved-thinking beta, which previously left an empty header value that some backends reject
- Web/Vis: Fix `kimi web` and `kimi vis` dying at startup on consoles whose codec cannot encode the banner arrow (GBK on Chinese Windows, for example). The banner is printed before the server binds its port, so the crash left nothing listening; unsupported characters are now replaced instead of raising

## 1.49.0 (2026-07-16)

Expand Down
21 changes: 21 additions & 0 deletions src/kimi_cli/utils/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import importlib
import socket
import sys
import textwrap


Expand Down Expand Up @@ -86,8 +87,28 @@ def get_network_addresses() -> list[str]:
return addresses


def _encodable(text: str) -> str:
"""Drop characters stdout cannot encode, so printing never raises.

The banner uses characters like U+279C that a legacy console codec (GBK on
Chinese Windows, for example) cannot represent. Printing those raises
UnicodeEncodeError, and since the banner is printed before the server binds
its port, an unhandled error there takes the whole process down.
"""
encoding = getattr(sys.stdout, "encoding", None) or "utf-8"
try:
text.encode(encoding)
except UnicodeEncodeError:
return text.encode(encoding, errors="replace").decode(encoding, errors="replace")
except LookupError:
return text
return text


def print_banner(lines: list[str]) -> None:
"""Print a boxed banner with tag conventions (<center>, <nowrap>, <hr>)."""
# Sanitize before measuring so the box borders still line up.
lines = [_encodable(line) for line in lines]
processed: list[str] = []
for line in lines:
if line == "<hr>":
Expand Down
39 changes: 39 additions & 0 deletions tests/core/test_print_banner_encoding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""print_banner must not raise on consoles whose codec lacks banner glyphs.

The banner is printed before the web/vis server binds its port, so an
unhandled UnicodeEncodeError there kills the process and the server never
starts.
"""

from __future__ import annotations

import contextlib
import io

from kimi_cli.utils.server import print_banner

# U+279C is what web/app.py and vis/app.py put in front of each URL.
_BANNER_LINE = "<nowrap> ➜ Local http://127.0.0.1:8000"


def _render(encoding: str) -> str:
"""Render the banner to a stream using the given console encoding."""
stream = io.TextIOWrapper(io.BytesIO(), encoding=encoding, errors="strict", newline="")
with contextlib.redirect_stdout(stream):
print_banner([_BANNER_LINE])
stream.flush()
return stream.buffer.getvalue().decode(encoding) # type: ignore[attr-defined]


def test_print_banner_survives_unencodable_glyph() -> None:
# gbk is the Chinese-locale Windows console codec and cannot encode U+279C.
assert "http://127.0.0.1:8000" in _render("gbk")


def test_print_banner_box_stays_aligned() -> None:
lines = [line for line in _render("gbk").splitlines() if line]
assert len({len(line) for line in lines}) == 1, lines


def test_print_banner_keeps_glyph_when_encoding_supports_it() -> None:
assert "➜" in _render("utf-8")