Skip to content

fix(kimi-web): migrate to www.kimi.com Connect-RPC API (kimi.moonshot.cn retired) - #5858

Merged
diegosouzapw merged 6 commits into
diegosouzapw:release/v3.8.43from
janeza2:fix/kimi-web-migrate-to-international
Jul 2, 2026
Merged

diegosouzapw merged 6 commits into
diegosouzapw:release/v3.8.43from
janeza2:fix/kimi-web-migrate-to-international

Conversation

@janeza2

@janeza2 janeza2 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

kimi-web is completely broken: the legacy kimi.moonshot.cn consumer chat domain was retired and now 307-redirects every non-CN visitor to https://www.kimi.com/, which speaks a completely different API. This PR repoints the provider at the international domain and reimplements the executor against the new wire format.

The earlier issue #5813 sketched the migration; this PR is the working implementation, validated end-to-end against a live Plus-tier overseas session.

New wire format (verified live)

Aspect Old (kimi.moonshot.cn) New (www.kimi.com)
Endpoint POST /api/chat POST /apiv2/kimi.gateway.chat.v1.ChatService/Chat
Protocol REST JSON Connect-RPC (application/connect+json)
Auth cookie kimi_token JWT in Authorization: Bearer … + Cookie: kimi-auth=<jwt>
Body plain JSON Connect streaming envelope — 5-byte header (flags + big-endian length) wrapping JSON. Without the envelope the upstream returns invalid_argument for every request.
Response OpenAI-style SSE Connect-framed stream of JSON events keyed by op + mask

Response event schema

op=set,     mask=block.text             → first answer chunk
op=append,  mask=block.text.content     → answer delta
op=set,     mask=block.think            → first reasoning chunk
op=append,  mask=block.think.content    → reasoning delta
op=set,     mask=block.stage / multiStage → stage transitions (suppressed)
{heartbeat:{}}                           → keep-alive (suppressed)
op=set,     message.role=assistant + status=MESSAGE_STATUS_COMPLETED → end of stream

The executor translates block.think.content deltas to OpenAI reasoning_content and block.text.content deltas to OpenAI content (SSE for streaming, buffered JSON for non-streaming).

Auth handling

Users paste the full Cookie header from www.kimi.com. The executor's extractKimiJwt accepts:

  • bare JWT
  • kimi-auth=<jwt> inside a full Cookie header
  • Cookie: or Authorization: Bearer prefixed forms

Only kimi-auth is replayed back to the upstream — analytics cookies (_ga, _gcl_au, __cf_bm, HM*, …) are dropped.

Headers — what is and isn't required

Tested against a live session by stripping headers one at a time:

Header Required?
Authorization: Bearer <jwt> ✅ yes
Cookie: kimi-auth=<jwt> ✅ yes
connect-protocol-version: 1 ✅ yes
Content-Type: application/connect+json ✅ yes (any other value → UnsupportedMediaType)
Connect framing on body ✅ yes (without it → invalid_argument)
x-msh-platform / x-msh-version ❌ not required
x-msh-device-id / x-msh-session-id / x-traffic-id ❌ not required
x-msh-shield-data ❌ not required (not the anti-bot gate it might look like)
x-language / r-timezone ❌ not required

The 4 "test matrix" runs (full headers / no bx-ua / no x-msh-* at all / minimal set) all produced the same Connect-framed event stream with model output (1+1=2).

Commits

  1. validator — new specialty validateKimiWebProvider probing GET /api/user; registered in the dispatcher so kimi-web no longer falls through to the generic OpenAI-compatible probe that hit the retired .cn host.
  2. executor + registry + metadata + docs — rewrites KimiWebExecutor for Connect-RPC; updates baseUrl, website/authHint, i18n strings (en, zh-CN, ru), tokenExtractionConfig, provider reference. Other locales were already in __MISSING__ fallback state and pick up the English source via the existing pipeline.
  3. tests — pins the executor URL (asserts www.kimi.com and explicitly !moonshot.cn), the missing-JWT 400 short-circuit, and the extractKimiJwt parser across all accepted input forms. Updates the existing web-cookie-sweep tests to feed a kimi-auth credential and assert the international URL.

Test plan

  • npm run typecheck:core — passes (only pre-existing unrelated appConfig.ts errors)
  • ReadLints — clean on all changed files (the two pre-existing node:test / duckduckgo-web type errors in web-cookie-providers-new.test.ts / webSessionCredentials.ts exist on upstream/main already)
  • Manual end-to-end against live www.kimi.com — chat completion returns the correct Connect-framed stream with reasoning + answer (1+1=2)
  • Manual matrix sweep of x-msh-* headers — confirmed not required
  • CI test-unit — needs upstream CI to run (local checkout lacks the cross-env / tsx runtime used by npm run test:unit)

Notes for reviewers

  • The model catalog is updated to { kimi-default, kimi-k2.6, kimi-128k }. The scenario value is pinned to SCENARIO_K2D5 (the only one the SPA sends today); if Kimi ships a new scenario we'll need to revisit.
  • The Connect frame parser is hand-rolled (5 bytes header + length-prefixed JSON). It is small enough that pulling in a Connect-RPC client dependency is not worth it.
  • The kimi-auth JWT is now treated as a long-lived bearer token. Expiry handling relies on the existing web-cookie refresh path — if the SPA rotates the JWT on the next login, the user re-pastes a fresh Cookie header (same UX as qwen-web).
  • Closes feat(providers): Add support for international domain (www.kimi.com) — currently CN-only #5813.

@janeza2
janeza2 requested a review from diegosouzapw as a code owner July 1, 2026 21:31
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

janeza2 added 4 commits July 2, 2026 05:16
…auth JWT

The legacy `kimi.moonshot.cn` consumer chat domain was retired in favor of
the international `www.kimi.com`. As part of that move the session probe
shifted from a cookie-based scheme on `.moonshot.cn` to a JWT bearer scheme:
the SPA stores the access JWT in the `kimi-auth` cookie and sends it as
`Authorization: Bearer <jwt>` on every API request. The new profile endpoint
is `GET /api/user` and returns the user object at the top level
(`{ id, name, email, region, ... }`).

This commit adds a specialty validator (`validateKimiWebProvider`) that
extracts `kimi-auth` from whatever the user pasted (bare JWT, full Cookie
header, or `Authorization: Bearer ...` form) and probes `/api/user` to
confirm the session. Mirrors the qwen-web validator pattern.

The dispatcher in `src/lib/providers/validation.ts` is updated to route
`kimi-web` through the new validator instead of falling through to the
generic OpenAI-compatible model-list probe (which was hitting a redirect
on the retired `.cn` host).
… API

The legacy `kimi.moonshot.cn/api/chat` REST endpoint is gone: the domain
307-redirects every non-CN visitor to `https://www.kimi.com/`, which serves
a completely different chat API based on Connect-RPC over HTTP.

New wire format (verified end-to-end against a live Plus-tier session):

- Endpoint: `POST /apiv2/kimi.gateway.chat.v1.ChatService/Chat`
- Auth: `Authorization: Bearer <jwt>` + `Cookie: kimi-auth=<jwt>`
- Body: Connect streaming envelope ΓÇö 5-byte header (1 byte flags +
  4 bytes big-endian length) wrapping the JSON request payload.
  Without the envelope the upstream returns `invalid_argument` for every
  request regardless of credentials.
- Response: a stream of Connect-framed JSON events with one of:
    op=set,    mask=block.text      → first answer chunk
    op=append, mask=block.text.content → answer delta
    op=set,    mask=block.think     → first reasoning chunk
    op=append, mask=block.think.content → reasoning delta
  plus heartbeats and metadata events that are suppressed.

The new executor:
- Parses the pasted Cookie header to extract `kimi-auth` (or accepts a
  bare JWT / `Authorization: Bearer` form).
- Frames the request body and parses the Connect event stream.
- Translates `block.think.content` deltas to OpenAI `reasoning_content`
  and `block.text.content` deltas to OpenAI `content` (SSE for streaming,
  buffered JSON for non-streaming).
- Sends only the `kimi-auth` cookie back rather than replaying the whole
  pasted Cookie header (avoids leaking analytics cookies like _ga, _gcl_au,
  __cf_bm, HM*, etc.).

The `x-msh-*` / `x-traffic-id` / `x-msh-shield-data` headers the SPA sends
are NOT required ΓÇö verified by stripping them one at a time against a live
session; the upstream returns the same response either way.

Registry, metadata, and docs are updated to point at the new domain and
document the new auth hint. The i18n description strings (en, zh-CN, ru)
are updated; other locales were already in the `__MISSING__` fallback
state and will pick up the English source via the existing pipeline.
Replaces the old `kimi.moonshot.cn` SSE-passthrough test (which no longer
reflects the wire format) with tests that pin:

- the executor now targets `https://www.kimi.com/...ChatService/Chat` and
  explicitly NOT `moonshot.cn`
- a missing/empty credential is rejected with 400 before fetch fires
- `extractKimiJwt` accepts bare JWT, `kimi-auth=<jwt>` inside a full
  Cookie header, `Cookie:` / `Authorization: Bearer` prefixed forms, and
  rejects inputs with no JWT

Updates `web-cookie-providers-new.test.ts` so the existing kimi-web
execution tests feed a fake `kimi-auth` credential (the executor now
requires one before fetching) and assert against the international URL.
…guard, dedupe parser)

Six follow-ups from code review on PR diegosouzapw#5858:

1. i18n regression (I1). The original commit updated only en/zh-CN/ru with
   the new "Moonshot AI consumer chat via www.kimi.com" description; the
   PR claimed the other 39 locales would fall back via the runtime
   pipeline, but `deepMergeFallback` only uses EN when the locale value
   is `undefined`. The 39 locales carried a literal
   `__MISSING__:Chinese market AI chat via kimi.moonshot.cn` string that
   would have rendered verbatim. Now the same 14 locales that previously
   had a `__MISSING__:` placeholder are updated to the new English source
   under `__MISSING__:` so the runtime coverage counter still flags them
   as missing translations while showing the correct text via the
   `__MISSING__:` fallback consumers.

2. Decoder tests (I3). The Connect frame decoder is the riskiest piece
   of the migration and was unverified. New `executor-kimi-web-decoder`
   test suite pins:
     - frameConnectMessage / decodeConnectFrame round-trip
     - partial-frame buffering (need-more-bytes path)
     - two-frame splitting (consumed offset semantics)
     - oversized-frame rejection (consumed=-1 sentinel)
     - sign-correction for lengths with bit 31 set
     - non-JSON payload handling
     - extractDelta across set/append x text/think, plus suppression of
       heartbeats and unrelated masks
     - isEndOfStream on assistant COMPLETED vs other roles/statuses
     - foldMessages with system/user/assistant/tool/function scenarios,
       including the documented single-turn limitation

3. DoS guard on Connect frame length (I4). Added MAX_FRAME_LEN (8 MiB)
   ceiling with a ponytail: comment naming the limit and upgrade path.
   decodeConnectFrame returns consumed=-1 when a header claims more,
   and both caller paths (streaming + non-streaming) treat -1 as
   stream-fatal ΓÇö streaming errors the controller, non-streaming stops
   accumulating and returns what it has.

4. reasoning_effort short-circuit (I5). A user sending
   `model: "kimi-k2.6"` + `reasoning_effort: "none"` previously still
   got thinking because the model-id regex beat the explicit override.
   Now reasoning_effort: "none" wins unconditionally.

5. foldMessages JSDoc limitation (I6). Documented that tool/function
   messages are dropped and that assistant tool_calls/image parts are
   stringified ΓÇö agentic flows should use the `kimi-coding` provider.

6. Dedupe JWT parser (M1+M2). extractKimiJwt moved to
   `src/lib/providers/webCookieAuth.ts` (alongside extractQwenToken),
   imported by both the executor and the validator. Eliminates the
   duplication that had drifted: the validator was missing the
   `bearer <jwt>` fallback the executor had, so a `Bearer eyJ...` paste
   would have executed but failed validation.
@janeza2
janeza2 force-pushed the fix/kimi-web-migrate-to-international branch from 5a00303 to 37e4681 Compare July 1, 2026 22:17
@diegosouzapw
diegosouzapw changed the base branch from main to release/v3.8.43 July 1, 2026 23:42
janeza2 and others added 2 commits July 1, 2026 21:06
…eb-migrate-to-international

Regenerated docs/reference/PROVIDER_REFERENCE.md after resolving the
merge conflict (scripts/docs/gen-provider-reference.ts).

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
The unit test suite imports extractKimiJwt via mod.extractKimiJwt
(destructured from the executor module), but the executor only
imported it from webCookieAuth without re-exporting it, leaving
5 test failures (TypeError: extractKimiJwt is not a function).

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
@diegosouzapw
diegosouzapw merged commit b708aa7 into diegosouzapw:release/v3.8.43 Jul 2, 2026
3 checks passed
@diegosouzapw

Copy link
Copy Markdown
Owner

Thanks @janeza2! Best PR of this review round — the Connect-RPC decoder is defensive (sign-extension, partial frames, 8MiB cap) and superbly tested. We resolved the release-branch conflicts, regenerated PROVIDER_REFERENCE, and added a one-line extractKimiJwt re-export the tests needed (kept you as author). Merged into release/v3.8.43. 🙌

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.

feat(providers): Add support for international domain (www.kimi.com) — currently CN-only

2 participants