pi-provider-kimi-code 0.6.5 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  Pi already has a basic `kimi-coding` provider. This extension is for the parts that start to matter once Kimi Code becomes part of your real Pi workflow: account login reuse, file uploads, tool schema compatibility, live model metadata, measured cache behavior, and API-tested parameter handling.
9
9
 
10
- > **Kimi K2.7 supported.** The provider automatically discovers the current model from Kimi's `/v1/models` endpoint at login, refresh, and `/kimi-settings` time, so new rollouts (including K2.7) are picked up immediately without a package update.
10
+ > **Kimi K3 supported.** The provider combines Kimi's live `/v1/models` catalog with your membership level from `/usages`, exposing only eligible models and advertising the correct K3 context limit. It refreshes this metadata at startup and through `/kimi-settings`; OAuth login and token refresh also update the model catalog.
11
11
 
12
12
  ## Why this exists
13
13
 
@@ -19,20 +19,20 @@ Kimi's API surface goes beyond the LLM itself. It includes file uploads, web sea
19
19
  - **Send files the Kimi way.** Large inline images go through Kimi's Files API and become `ms://` references instead of huge base64 payloads.
20
20
  - **Know what the cache is doing.** Kimi caches by content prefix. This repo measures that behavior instead of pretending `prompt_cache_key` controls it.
21
21
  - **Keep Pi's tools working.** Moonshot's API rejects tool schemas over 15 KB — a limit that Pi's extension ecosystem regularly hits ([#16](https://github.com/Leechael/pi-provider-kimi-code/issues/16), [#21](https://github.com/Leechael/pi-provider-kimi-code/issues/21)). This extension automatically deduplicates schemas with `$ref`/`$defs` before sending, so subagents and other extensions don't break.
22
- - **Tested against the live API.** Thinking config, parameter constraints, protocol compatibility, and streaming behavior are all verified against Kimi's Coding endpoint — not assumed from docs. When the API rejects something (wrong `temperature`, unsupported `tool_choice`), the provider strips it before you see a 400.
22
+ - **Tested against the live API.** Thinking config, parameter constraints, protocol compatibility, and streaming behavior are all verified against Kimi's Coding endpoint — not assumed from docs. When the API rejects something (wrong `temperature`, unsupported `tool_choice`), the provider normalizes the request before you see a 400.
23
23
  - **Turn on Kimi-native tools when you want them.** `moonshot_search`, `moonshot_fetch`, and `kimi_datasource` are opt-in, configurable per user or per project.
24
24
  - **Embed in your own build.** `KimiCode()` factory lets you ship Kimi Code support inside a custom Pi agent with programmatic config overrides — no file-based extension path needed.
25
25
 
26
26
  ## What this package adds
27
27
 
28
28
  - Kimi account login in Pi, plus `KIMI_API_KEY` for CI or pay-per-token use.
29
- - `kimi-code` credential reuse from `~/.kimi/credentials/kimi-code.json`.
29
+ - `kimi-code` credential reuse from `~/.kimi-code/credentials/kimi-code.json`, with read-only support for the legacy `~/.kimi` path.
30
30
  - Kimi Files API uploads for large inline images.
31
- - Live model metadata from Kimi at login, refresh, and `/kimi-settings` time.
31
+ - Live model metadata combined with membership-aware K3 and HighSpeed availability.
32
32
  - OpenAI-compatible mode by default, Anthropic-compatible mode on request.
33
- - Kimi K2.7 reasoning level mapping for Pi's reasoning controls.
33
+ - K2.7 and K3 reasoning level mapping for Pi's reasoning controls.
34
34
  - Tool schema dedup to stay under Moonshot's 15 KB per-tool-schema limit.
35
- - K2.7 parameter guard: strips `temperature`, `top_p`, and `tool_choice` values that the API rejects.
35
+ - K2.7 parameter guard: removes rejected `temperature` and `top_p` values and rewrites unsupported `tool_choice` values to `auto`.
36
36
  - Stream cleanup for Kimi's thinking-only placeholder text.
37
37
  - Optional `moonshot_search`, `moonshot_fetch`, and unified datasource `kimi_datasource` tools via `/kimi-settings`.
38
38
  - Programmatic `KimiCode()` factory for embedding in custom Pi builds.
@@ -84,15 +84,21 @@ Inside Pi, run:
84
84
  /login kimi-coding
85
85
  ```
86
86
 
87
- A browser tab opens, you sign into your Kimi account, and Pi stores the credential at `~/.pi/agent/auth.json`. Tokens refresh automatically.
87
+ A browser tab opens, you sign into your Kimi account, and Pi stores the credential at `~/.pi/agent/auth.json`. Tokens refresh automatically. The extension also syncs refreshed credentials to the current `kimi-code` credential file:
88
88
 
89
- If you already use `kimi-code`, this extension can reuse its credential file:
89
+ ```text
90
+ ~/.kimi-code/credentials/kimi-code.json
91
+ ```
92
+
93
+ If you already use `kimi-code`, the extension can reuse that session. Set `KIMI_CODE_HOME` if its home directory lives somewhere else.
94
+
95
+ The legacy credential path is also supported:
90
96
 
91
97
  ```text
92
98
  ~/.kimi/credentials/kimi-code.json
93
99
  ```
94
100
 
95
- The file is read-only from this extension's side. Set `KIMI_SHARE_DIR` if your `kimi-code` share directory lives somewhere else.
101
+ Legacy credentials are read-only. Set `KIMI_SHARE_DIR` to override the legacy `~/.kimi` directory.
96
102
 
97
103
  For CI or pay-per-token use, set `KIMI_API_KEY` instead:
98
104
 
@@ -100,20 +106,24 @@ For CI or pay-per-token use, set `KIMI_API_KEY` instead:
100
106
  KIMI_API_KEY=sk-... pi
101
107
  ```
102
108
 
103
- ## Model
109
+ ## Models
104
110
 
105
- This provider publishes two Pi model IDs:
111
+ This provider publishes three Pi model IDs:
106
112
 
107
- ```text
108
- kimi-coding/kimi-for-coding
109
- kimi-coding/kimi-for-coding-highspeed
110
- ```
113
+ | Pi model ID | Upstream model | Access |
114
+ | --------------------------------------- | ------------------------ | ------------------------------------------------------ |
115
+ | `kimi-coding/kimi-for-coding` | Kimi K2.7 Code | All Kimi Code members |
116
+ | `kimi-coding/kimi-for-coding-highspeed` | Kimi K2.7 Code HighSpeed | Allegretto and above |
117
+ | `kimi-coding/k3` | Kimi K3 | Moderato: 256K context; Allegretto and above: up to 1M |
111
118
 
112
- Select standard or highspeed mode inside Pi:
119
+ Plans below Moderato cannot use K3. The provider reads the current membership level from `/usages` and combines it with `/models`: server catalog availability remains authoritative, while known plan limits can only hide unavailable selections or lower K3's advertised context window. Unknown or unavailable membership data falls back to the server catalog instead of guessing.
120
+
121
+ Select a model inside Pi:
113
122
 
114
123
  ```text
115
124
  /model kimi-coding/kimi-for-coding
116
125
  /model kimi-coding/kimi-for-coding-highspeed
126
+ /model kimi-coding/k3
117
127
  ```
118
128
 
119
129
  Kimi keeps Coding models behind aliases. Rather than hardcoding a stale model list, this extension asks Kimi for the current model info when you log in or refresh. If your account is on a newer rollout (e.g., Kimi K2.7) or internal test, Pi can pick up the latest model name and context size without waiting for a package release.
@@ -125,7 +135,9 @@ Fallback values:
125
135
  - Input: text and image
126
136
  - Reasoning: enabled
127
137
 
128
- The provider maps Pi's reasoning levels to Kimi's top-level `thinking` parameter. It sends `thinking.effort` only when `/models` advertises supported effort values. The mapping refreshes automatically on credential refresh. Opening `/kimi-settings` also re-discovers the latest model metadata.
138
+ The provider maps Pi's reasoning levels to Kimi's top-level `thinking` parameter. K3 currently advertises only `max`; `max` and `xhigh` map to `max`, `high` and `medium` map to `high`, and `low` and `minimal` map to `low`. It sends `thinking.effort` only when `/models` advertises the mapped value. The mapping refreshes automatically on credential refresh. Opening `/kimi-settings` also re-discovers the latest model and membership metadata.
139
+
140
+ Switching models or thinking effort invalidates Kimi's existing context cache. Start a new session when switching to avoid re-prefilling a long conversation and consuming extra quota.
129
141
 
130
142
  ## Optional tools
131
143
 
@@ -143,8 +155,8 @@ Configurable settings include protocol mode, upload threshold, and per-tool enab
143
155
 
144
156
  Config files are JSON:
145
157
 
146
- - Home: `~/.pi/pi-provider-kimi-code.json`
147
- - Project override: `<cwd>/.pi/pi-provider-kimi-code.json`
158
+ - Home: `~/.pi/providers/kimi-coding/config.json`
159
+ - Project override: `<cwd>/.pi/providers/kimi-coding/config.json`
148
160
 
149
161
  Project config overrides home config with a deep merge. Missing files or missing keys mean all tools stay off.
150
162
 
@@ -296,7 +308,7 @@ In OpenAI mode this extension maps the `developer` role to `system` (Kimi's Codi
296
308
 
297
309
  ## Credits
298
310
 
299
- Based on the login implementation from Kimi Code by Moonshot AI. Built as a Pi extension by [@badlogic](https://github.com/badlogic).
311
+ Based on the login implementation from Kimi Code by Moonshot AI. Pi was originally created by [@badlogic](https://github.com/badlogic).
300
312
 
301
313
  ## License
302
314
 
package/index.ts CHANGED
@@ -42,12 +42,16 @@ import { PROVIDER_ID, PROVIDER_VERSION, getBaseUrl, getKimiApiType } from "./src
42
42
  import {
43
43
  type KimiOAuthCredentials,
44
44
  type KimiOAuthExtras,
45
+ applyKimiMembershipLimitsToModel,
45
46
  buildKimiModelFromConfig,
46
47
  applyKimiOAuthExtrasToModel,
47
48
  KIMI_CODING_HIGHSPEED_MODEL_ID,
48
49
  KIMI_CODING_MODEL_ID,
50
+ KIMI_K3_MODEL_ID,
51
+ KIMI_MODEL_CATALOG_VERSION,
49
52
  discoverKimiModelMetadata,
50
53
  getKimiModelMetadata,
54
+ isKimiModelAvailableForMembership,
51
55
  resolveKimiModelConfig,
52
56
  } from "./src/models.ts";
53
57
  import { getKimiApiKey, loginKimiCode, refreshKimiCodeToken } from "./src/oauth.ts";
@@ -60,13 +64,14 @@ import {
60
64
  parseByteSizeInput,
61
65
  } from "./src/settings-ui.ts";
62
66
  import { setStoreResolvedKimiConfig, streamSimpleKimi } from "./src/stream.ts";
63
- import { fetchKimiUsageSummary, getKimiUsageToken } from "./src/usage.ts";
67
+ import { fetchKimiUsageSnapshot, getKimiUsageToken } from "./src/usage.ts";
64
68
  import { buildMoonshotFetchTool, buildMoonshotSearchTool } from "./src/tools/moonshot.ts";
65
69
  import { buildKimiDatasourceTool } from "./src/tools/datasource.ts";
66
70
  interface KimiRuntimeState {
67
71
  cwd: string;
68
72
  config: KimiCodeConfig;
69
73
  modelExtras: KimiOAuthExtras;
74
+ membershipLevel: string | null;
70
75
  projectTrusted: boolean;
71
76
  overrides?: KimiCodeConfigPatch;
72
77
  }
@@ -152,11 +157,21 @@ async function openSettingsMenu(
152
157
  ctx: ExtensionCommandContext,
153
158
  state: KimiRuntimeState,
154
159
  ): Promise<void> {
155
- const [usage, modelsRefreshed] = await Promise.all([
156
- fetchKimiUsageSummary(),
160
+ const modelDiscoveryToken = getKimiUsageToken();
161
+ const [usageSnapshot, initialModelsRefreshed] = await Promise.all([
162
+ fetchKimiUsageSnapshot(),
157
163
  refreshModelExtras(state),
158
164
  ]);
159
- if (modelsRefreshed) registerKimiProvider(pi, state);
165
+ let modelsRefreshed = initialModelsRefreshed;
166
+ const refreshedToken = getKimiUsageToken();
167
+ if (!modelsRefreshed && refreshedToken && refreshedToken !== modelDiscoveryToken) {
168
+ modelsRefreshed = await refreshModelExtras(state);
169
+ }
170
+ const refreshedMembershipLevel = usageSnapshot.membershipLevel ?? state.membershipLevel;
171
+ const membershipChanged = state.membershipLevel !== refreshedMembershipLevel;
172
+ state.membershipLevel = refreshedMembershipLevel;
173
+ if (modelsRefreshed || membershipChanged) registerKimiProvider(pi, state);
174
+ const usage = usageSnapshot.summary;
160
175
 
161
176
  const projectTrusted = await isKimiProjectConfigApproved(ctx, ctx.cwd);
162
177
  const homeDraft = loadHomeKimiCodeConfig(os.homedir());
@@ -367,19 +382,40 @@ function saveScopeKimiCodeConfig(
367
382
  function filterAvailableKimiModels<T extends { id: string }>(
368
383
  models: T[],
369
384
  extras: KimiOAuthExtras,
385
+ membershipLevel: string | null,
370
386
  ): T[] {
371
- const available = extras.modelCatalog ? new Set(Object.keys(extras.modelCatalog)) : null;
372
- return available ? models.filter((model) => available.has(model.id)) : models;
387
+ const available =
388
+ extras.modelCatalogVersion === KIMI_MODEL_CATALOG_VERSION && extras.modelCatalog
389
+ ? new Set(Object.keys(extras.modelCatalog))
390
+ : null;
391
+ return models.filter(
392
+ (model) =>
393
+ (!available || available.has(model.id)) &&
394
+ isKimiModelAvailableForMembership(model.id, membershipLevel) !== false,
395
+ );
373
396
  }
374
397
 
375
398
  function registerKimiProvider(pi: ExtensionAPI, state: KimiRuntimeState): void {
376
- const standardModel = applyKimiOAuthExtrasToModel(
377
- buildKimiModelFromConfig(state.config.model),
378
- getKimiModelMetadata(state.modelExtras, KIMI_CODING_MODEL_ID),
399
+ const standardModel = applyKimiMembershipLimitsToModel(
400
+ applyKimiOAuthExtrasToModel(
401
+ buildKimiModelFromConfig(state.config.model),
402
+ getKimiModelMetadata(state.modelExtras, KIMI_CODING_MODEL_ID),
403
+ ),
404
+ state.membershipLevel,
379
405
  );
380
- const highSpeedModel = applyKimiOAuthExtrasToModel(
381
- buildKimiModelFromConfig(state.config.model, KIMI_CODING_HIGHSPEED_MODEL_ID),
382
- getKimiModelMetadata(state.modelExtras, KIMI_CODING_HIGHSPEED_MODEL_ID),
406
+ const highSpeedModel = applyKimiMembershipLimitsToModel(
407
+ applyKimiOAuthExtrasToModel(
408
+ buildKimiModelFromConfig(state.config.model, KIMI_CODING_HIGHSPEED_MODEL_ID),
409
+ getKimiModelMetadata(state.modelExtras, KIMI_CODING_HIGHSPEED_MODEL_ID),
410
+ ),
411
+ state.membershipLevel,
412
+ );
413
+ const k3Model = applyKimiMembershipLimitsToModel(
414
+ applyKimiOAuthExtrasToModel(
415
+ buildKimiModelFromConfig(state.config.model, KIMI_K3_MODEL_ID),
416
+ getKimiModelMetadata(state.modelExtras, KIMI_K3_MODEL_ID),
417
+ ),
418
+ state.membershipLevel,
383
419
  );
384
420
 
385
421
  pi.registerProvider(PROVIDER_ID, {
@@ -388,12 +424,34 @@ function registerKimiProvider(pi: ExtensionAPI, state: KimiRuntimeState): void {
388
424
  api: getKimiApiType(state.config.protocol),
389
425
  streamSimple: streamSimpleKimi,
390
426
 
391
- models: filterAvailableKimiModels([standardModel, highSpeedModel], state.modelExtras),
427
+ models: filterAvailableKimiModels(
428
+ [standardModel, highSpeedModel, k3Model],
429
+ state.modelExtras,
430
+ state.membershipLevel,
431
+ ),
392
432
 
393
433
  oauth: {
394
434
  name: "Kimi Code (OAuth)",
395
- login: loginKimiCode,
396
- refreshToken: refreshKimiCodeToken,
435
+ login: async (callbacks) => {
436
+ const credentials = await loginKimiCode(callbacks);
437
+ const usage = await fetchKimiUsageSnapshot({
438
+ timeoutMs: 2500,
439
+ token: credentials.access,
440
+ refreshOnUnauthorized: false,
441
+ });
442
+ state.membershipLevel = usage.membershipLevel;
443
+ return credentials;
444
+ },
445
+ refreshToken: async (credentials) => {
446
+ const refreshed = await refreshKimiCodeToken(credentials);
447
+ const usage = await fetchKimiUsageSnapshot({
448
+ timeoutMs: 2500,
449
+ token: refreshed.access,
450
+ refreshOnUnauthorized: false,
451
+ });
452
+ state.membershipLevel = usage.membershipLevel;
453
+ return refreshed;
454
+ },
397
455
  getApiKey: getKimiApiKey,
398
456
  // Reflect server-side model identity on the registered model after login
399
457
  // / refresh. We never rewrite the model id (pi-side `/model` selections
@@ -404,14 +462,23 @@ function registerKimiProvider(pi: ExtensionAPI, state: KimiRuntimeState): void {
404
462
  const extras = cred as KimiOAuthCredentials;
405
463
  state.modelExtras = extras;
406
464
  reloadEffectiveKimiRuntimeConfig(state, state.cwd, state.projectTrusted);
407
- const available = extras.modelCatalog ? new Set(Object.keys(extras.modelCatalog)) : null;
465
+ const available =
466
+ extras.modelCatalogVersion === KIMI_MODEL_CATALOG_VERSION && extras.modelCatalog
467
+ ? new Set(Object.keys(extras.modelCatalog))
468
+ : null;
408
469
  return models
409
470
  .filter(
410
- (model) => model.provider !== PROVIDER_ID || !available || available.has(model.id),
471
+ (model) =>
472
+ model.provider !== PROVIDER_ID ||
473
+ ((!available || available.has(model.id)) &&
474
+ isKimiModelAvailableForMembership(model.id, state.membershipLevel) !== false),
411
475
  )
412
476
  .map((model) =>
413
477
  model.provider === PROVIDER_ID
414
- ? applyKimiOAuthExtrasToModel(model, getKimiModelMetadata(extras, model.id))
478
+ ? applyKimiMembershipLimitsToModel(
479
+ applyKimiOAuthExtrasToModel(model, getKimiModelMetadata(extras, model.id)),
480
+ state.membershipLevel,
481
+ )
415
482
  : model,
416
483
  );
417
484
  },
@@ -427,13 +494,24 @@ export function KimiCode(overrides?: KimiCodeConfigPatch): ExtensionFactory {
427
494
  overrides,
428
495
  );
429
496
  const discoveryToken = getKimiUsageToken();
430
- const discovered = discoveryToken
431
- ? await discoverKimiModelMetadata(discoveryToken, config.protocol)
432
- : {};
497
+ const [initialDiscovery, usageSnapshot] = await Promise.all([
498
+ discoveryToken ? discoverKimiModelMetadata(discoveryToken, config.protocol) : {},
499
+ fetchKimiUsageSnapshot({ timeoutMs: 2500 }),
500
+ ]);
501
+ let discovered = initialDiscovery;
502
+ const refreshedToken = getKimiUsageToken();
503
+ if (
504
+ Object.keys(discovered).length === 0 &&
505
+ refreshedToken &&
506
+ refreshedToken !== discoveryToken
507
+ ) {
508
+ discovered = await discoverKimiModelMetadata(refreshedToken, config.protocol);
509
+ }
433
510
  const state: KimiRuntimeState = {
434
511
  cwd,
435
512
  config,
436
513
  modelExtras: discovered,
514
+ membershipLevel: usageSnapshot.membershipLevel,
437
515
  projectTrusted: false,
438
516
  overrides,
439
517
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-kimi-code",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "description": "Use Kimi Code in Pi without losing the Kimi-specific bits",
5
5
  "keywords": [
6
6
  "ai-provider",
package/src/config.ts CHANGED
@@ -121,9 +121,10 @@ export const DEFAULT_KIMI_CODE_CONFIG: KimiCodeConfig = {
121
121
  off: { effort: null, enabled: false },
122
122
  minimal: { effort: "low", enabled: true },
123
123
  low: { effort: "low", enabled: true },
124
- medium: { effort: "medium", enabled: true },
124
+ medium: { effort: "high", enabled: true },
125
125
  high: { effort: "high", enabled: true },
126
- xhigh: { effort: "high", enabled: true },
126
+ xhigh: { effort: "max", enabled: true },
127
+ max: { effort: "max", enabled: true },
127
128
  },
128
129
  thinkingKeep: "all",
129
130
  generation: {},
package/src/constants.ts CHANGED
@@ -41,12 +41,12 @@ export function getDefaultBaseUrl(protocol: KimiWireProtocol): string {
41
41
 
42
42
  export const DEFAULT_BASE_URL = getDefaultBaseUrl(ENV_KIMI_CODE_PROTOCOL);
43
43
 
44
- export const PROVIDER_VERSION = "0.6.5";
44
+ export const PROVIDER_VERSION = "0.6.6";
45
45
 
46
46
  // Upstream kimi-code CLI version — used in User-Agent and X-Msh-Version
47
47
  // headers to match the official client's identity. Update this when
48
- // syncing with https://github.com/anthropics/kimi-code.
49
- export const KIMI_UPSTREAM_VERSION = "0.23.4";
48
+ // syncing with https://github.com/MoonshotAI/kimi-code.
49
+ export const KIMI_UPSTREAM_VERSION = "0.26.0";
50
50
  export const KIMI_CODE_USER_AGENT = `kimi-code-cli/${KIMI_UPSTREAM_VERSION}`;
51
51
  export const KIMI_PLATFORM = "kimi_code_cli";
52
52
 
package/src/models.ts CHANGED
@@ -22,6 +22,7 @@ export interface KimiModelMetadata {
22
22
 
23
23
  export interface KimiOAuthExtras extends KimiModelMetadata {
24
24
  modelCatalog?: Record<string, KimiModelMetadata>;
25
+ modelCatalogVersion?: number;
25
26
  }
26
27
 
27
28
  export type KimiOAuthCredentials = OAuthCredentials & KimiOAuthExtras;
@@ -56,6 +57,46 @@ const COST_HIGH_SPEED = { input: 1.793, output: 7.448, cacheRead: 0.359, cacheWr
56
57
 
57
58
  export const KIMI_CODING_MODEL_ID = "kimi-for-coding";
58
59
  export const KIMI_CODING_HIGHSPEED_MODEL_ID = "kimi-for-coding-highspeed";
60
+ export const KIMI_K3_MODEL_ID = "k3";
61
+ export const KIMI_MODEL_CATALOG_VERSION = 1;
62
+
63
+ const KIMI_K3_MODERATO_CONTEXT_WINDOW = 262144;
64
+ const KIMI_MEMBERSHIP_RANK: Readonly<Record<string, number>> = {
65
+ LEVEL_FREE: 0,
66
+ LEVEL_BASIC: 1,
67
+ LEVEL_STANDARD: 2,
68
+ LEVEL_INTERMEDIATE: 3,
69
+ LEVEL_ADVANCED: 4,
70
+ LEVEL_PREMIUM: 5,
71
+ };
72
+ const KIMI_MODERATO_RANK = KIMI_MEMBERSHIP_RANK.LEVEL_STANDARD;
73
+ const KIMI_ALLEGRETTO_RANK = KIMI_MEMBERSHIP_RANK.LEVEL_INTERMEDIATE;
74
+
75
+ export function isKimiModelAvailableForMembership(
76
+ modelId: string,
77
+ membershipLevel: string | null | undefined,
78
+ ): boolean | undefined {
79
+ if (!membershipLevel) return undefined;
80
+ const rank = KIMI_MEMBERSHIP_RANK[membershipLevel];
81
+ if (rank === undefined) return undefined;
82
+ if (modelId === KIMI_K3_MODEL_ID) return rank >= KIMI_MODERATO_RANK;
83
+ if (modelId === KIMI_CODING_HIGHSPEED_MODEL_ID) return rank >= KIMI_ALLEGRETTO_RANK;
84
+ return true;
85
+ }
86
+
87
+ export function applyKimiMembershipLimitsToModel(
88
+ model: Model<Api>,
89
+ membershipLevel: string | null | undefined,
90
+ ): Model<Api> {
91
+ const rank = membershipLevel ? KIMI_MEMBERSHIP_RANK[membershipLevel] : undefined;
92
+ if (model.id !== KIMI_K3_MODEL_ID || rank === undefined || rank >= KIMI_ALLEGRETTO_RANK) {
93
+ return model;
94
+ }
95
+ return {
96
+ ...model,
97
+ contextWindow: Math.min(model.contextWindow, KIMI_K3_MODERATO_CONTEXT_WINDOW),
98
+ };
99
+ }
59
100
 
60
101
  function resolveModelCost(modelDisplay: string | undefined): {
61
102
  input: number;
@@ -72,9 +113,15 @@ export function buildKimiModelFromConfig(
72
113
  modelId = KIMI_CODING_MODEL_ID,
73
114
  ): Model<Api> {
74
115
  const isHighSpeed = modelId === KIMI_CODING_HIGHSPEED_MODEL_ID;
116
+ const name =
117
+ modelId === KIMI_K3_MODEL_ID
118
+ ? "Kimi K3"
119
+ : isHighSpeed
120
+ ? "Kimi for Coding High Speed"
121
+ : "Kimi for Coding";
75
122
  return {
76
123
  id: modelId,
77
- name: isHighSpeed ? "Kimi for Coding High Speed" : "Kimi for Coding",
124
+ name,
78
125
  reasoning: config.reasoning,
79
126
  input: [...config.input] as unknown as ("text" | "image" | "video")[],
80
127
  cost: { ...(isHighSpeed ? COST_HIGH_SPEED : COST_STANDARD) },
@@ -201,7 +248,11 @@ export async function discoverKimiModelMetadata(
201
248
  if (!response.ok) return {};
202
249
  const json = (await response.json()) as { data?: unknown };
203
250
  const list = Array.isArray(json.data) ? (json.data as KimiServerModel[]) : [];
204
- const supportedIds = new Set([KIMI_CODING_MODEL_ID, KIMI_CODING_HIGHSPEED_MODEL_ID]);
251
+ const supportedIds = new Set([
252
+ KIMI_CODING_MODEL_ID,
253
+ KIMI_CODING_HIGHSPEED_MODEL_ID,
254
+ KIMI_K3_MODEL_ID,
255
+ ]);
205
256
  const modelCatalog: Record<string, KimiModelMetadata> = {};
206
257
  for (const model of list) {
207
258
  if (typeof model.id !== "string" || !supportedIds.has(model.id)) continue;
@@ -210,9 +261,11 @@ export async function discoverKimiModelMetadata(
210
261
  }
211
262
  const preferred = modelCatalog[KIMI_CODING_MODEL_ID];
212
263
  if (!preferred) {
213
- return Object.keys(modelCatalog).length > 0 ? { modelCatalog } : {};
264
+ return Object.keys(modelCatalog).length > 0
265
+ ? { modelCatalog, modelCatalogVersion: KIMI_MODEL_CATALOG_VERSION }
266
+ : {};
214
267
  }
215
- return { ...preferred, modelCatalog };
268
+ return { ...preferred, modelCatalog, modelCatalogVersion: KIMI_MODEL_CATALOG_VERSION };
216
269
  } catch {
217
270
  return {};
218
271
  } finally {
@@ -238,7 +291,10 @@ export function applyKimiOAuthExtrasToModel(
238
291
  defaultEffort?: string;
239
292
  } = { ...model };
240
293
  if (typeof extras.modelDisplay === "string" && extras.modelDisplay) {
241
- next.name = extras.modelDisplay;
294
+ next.name =
295
+ model.id === KIMI_K3_MODEL_ID && /^k3$/i.test(extras.modelDisplay)
296
+ ? "Kimi K3"
297
+ : extras.modelDisplay;
242
298
  next.cost = resolveModelCost(extras.modelDisplay);
243
299
  }
244
300
  if (typeof extras.contextLength === "number" && extras.contextLength > 0) {
package/src/oauth.ts CHANGED
@@ -120,6 +120,7 @@ export async function requestDeviceToken(
120
120
 
121
121
  interface RefreshAccessTokenOptions {
122
122
  maxRetries?: number;
123
+ signal?: AbortSignal;
123
124
  sleep?: (ms: number) => Promise<void>;
124
125
  }
125
126
 
@@ -133,8 +134,20 @@ export class KimiLoginRequiredError extends Error {
133
134
  }
134
135
  }
135
136
 
136
- function sleep(ms: number): Promise<void> {
137
- return new Promise((resolve) => setTimeout(resolve, ms));
137
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
138
+ if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
139
+ signal.throwIfAborted();
140
+ return new Promise((resolve, reject) => {
141
+ const onAbort = () => {
142
+ clearTimeout(timeout);
143
+ reject(signal.reason);
144
+ };
145
+ const timeout = setTimeout(() => {
146
+ signal.removeEventListener("abort", onAbort);
147
+ resolve();
148
+ }, ms);
149
+ signal.addEventListener("abort", onAbort, { once: true });
150
+ });
138
151
  }
139
152
 
140
153
  export async function refreshAccessToken(
@@ -142,11 +155,12 @@ export async function refreshAccessToken(
142
155
  options: RefreshAccessTokenOptions = {},
143
156
  ): Promise<TokenResponse> {
144
157
  const maxRetries = options.maxRetries ?? 3;
145
- const wait = options.sleep ?? sleep;
158
+ const wait = options.sleep ?? ((ms: number) => sleep(ms, options.signal));
146
159
  let lastError: unknown;
147
160
 
148
161
  for (let attempt = 0; attempt < maxRetries; attempt++) {
149
162
  try {
163
+ options.signal?.throwIfAborted();
150
164
  const response = await fetch(`${getOAuthHost()}/api/oauth/token`, {
151
165
  method: "POST",
152
166
  headers: {
@@ -158,6 +172,7 @@ export async function refreshAccessToken(
158
172
  grant_type: "refresh_token",
159
173
  refresh_token: refreshToken,
160
174
  }),
175
+ signal: options.signal,
161
176
  });
162
177
 
163
178
  if (!response.ok) {
@@ -185,6 +200,7 @@ export async function refreshAccessToken(
185
200
 
186
201
  return data;
187
202
  } catch (error) {
203
+ if (options.signal?.aborted) throw options.signal.reason;
188
204
  lastError = error;
189
205
  const message = error instanceof Error ? error.message : String(error);
190
206
  if (error instanceof KimiLoginRequiredError) throw error;
@@ -418,6 +434,11 @@ export function getKimiApiKey(credentials: OAuthCredentials): string {
418
434
 
419
435
  export function isKimiAuthErrorMessage(message: unknown): boolean {
420
436
  const text = String(message ?? "").toLowerCase();
437
+ const isMembershipPermissionError =
438
+ text.includes("current subscription does not have access to k3") ||
439
+ text.includes("current plan supports only kimi-k3 up to 256k context") ||
440
+ text.includes("current subscription does not have access to kimi-for-coding-highspeed");
441
+ if (isMembershipPermissionError) return false;
421
442
  return (
422
443
  /\b401\b/.test(text) ||
423
444
  text.includes("unauthorized") ||
@@ -426,7 +447,10 @@ export function isKimiAuthErrorMessage(message: unknown): boolean {
426
447
  );
427
448
  }
428
449
 
429
- export async function refreshKimiAuthToken(currentKey: string): Promise<string | null> {
450
+ export async function refreshKimiAuthToken(
451
+ currentKey: string,
452
+ options: { signal?: AbortSignal } = {},
453
+ ): Promise<string | null> {
430
454
  try {
431
455
  const kimiCred = readKimiCliCredentials();
432
456
  const storage = AuthStorage.create();
@@ -442,7 +466,7 @@ export async function refreshKimiAuthToken(currentKey: string): Promise<string |
442
466
  console.error("[kimi-coding] auth refresh: requesting new access token");
443
467
  let refreshed: Awaited<ReturnType<typeof refreshAccessToken>>;
444
468
  try {
445
- refreshed = await refreshAccessToken(piOAuth.refresh);
469
+ refreshed = await refreshAccessToken(piOAuth.refresh, { signal: options.signal });
446
470
  } catch (error) {
447
471
  if (!kimiCred?.refresh_token || kimiCred.refresh_token === piOAuth.refresh) throw error;
448
472
  const kimiExpiresMs = (kimiCred.expires_at ?? 0) * 1000;
@@ -459,7 +483,7 @@ export async function refreshKimiAuthToken(currentKey: string): Promise<string |
459
483
  return recovered.access;
460
484
  }
461
485
  console.error("[kimi-coding] auth refresh: pi token rejected, trying kimi-code token");
462
- refreshed = await refreshAccessToken(kimiCred.refresh_token);
486
+ refreshed = await refreshAccessToken(kimiCred.refresh_token, { signal: options.signal });
463
487
  }
464
488
 
465
489
  const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
@@ -490,12 +514,15 @@ export async function refreshKimiAuthToken(currentKey: string): Promise<string |
490
514
  }
491
515
 
492
516
  console.error("[kimi-coding] auth refresh: requesting new access token via kimi-code");
493
- const refreshed = await refreshAccessToken(kimiCred.refresh_token!);
517
+ const refreshed = await refreshAccessToken(kimiCred.refresh_token!, {
518
+ signal: options.signal,
519
+ });
494
520
  const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
495
521
  writeKimiCodeCredentials(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
496
522
  console.error("[kimi-coding] auth refresh: new token persisted to kimi-code");
497
523
  return refreshed.access_token;
498
524
  } catch (err) {
525
+ if (options.signal?.aborted) return null;
499
526
  if (err instanceof KimiLoginRequiredError) {
500
527
  console.error(`[kimi-coding] ${KIMI_LOGIN_REQUIRED_MESSAGE}`);
501
528
  } else {
package/src/payload.ts CHANGED
@@ -488,6 +488,28 @@ export async function applyKimiPayloadMutations(
488
488
  }
489
489
  }
490
490
 
491
+ // Preserved-thinking Kimi endpoints require every replayed assistant turn to
492
+ // carry reasoning_content, including turns whose reasoning delta was empty.
493
+ // pi-ai drops empty thinking blocks while building Chat Completions history,
494
+ // so restore the explicit empty field after the final thinking mode is known.
495
+ if (
496
+ ctx.api === "openai-completions" &&
497
+ isRecord(payload.thinking) &&
498
+ payload.thinking.type !== "disabled" &&
499
+ payload.thinking.keep === "all" &&
500
+ Array.isArray(payload.messages)
501
+ ) {
502
+ for (const message of payload.messages) {
503
+ if (
504
+ isRecord(message) &&
505
+ message.role === "assistant" &&
506
+ message.reasoning_content === undefined
507
+ ) {
508
+ message.reasoning_content = "";
509
+ }
510
+ }
511
+ }
512
+
491
513
  // 8. K2.7 Code API constraints: the server rejects non-default values for
492
514
  // temperature (must be 1.0) and top_p (must be 0.95), and tool_choice
493
515
  // "required" / function-specific when thinking is enabled (always-on).
package/src/usage.ts CHANGED
@@ -36,6 +36,11 @@ export interface UsageFormatOptions {
36
36
  timeZone?: string;
37
37
  }
38
38
 
39
+ export interface KimiUsageSnapshot {
40
+ summary: string;
41
+ membershipLevel: string | null;
42
+ }
43
+
39
44
  interface BoosterWalletInfo {
40
45
  balanceCents: bigint;
41
46
  monthlyChargeLimitEnabled: boolean;
@@ -232,7 +237,7 @@ function formatWindowLabel(value: unknown, fallbackLabel: string): string {
232
237
  return `Current ${minutes}m window`;
233
238
  }
234
239
 
235
- export function parseMembership(record: Record<string, unknown>): string | null {
240
+ export function parseMembershipLevel(record: Record<string, unknown>): string | null {
236
241
  const user = record.user;
237
242
  if (typeof user !== "object" || user === null || Array.isArray(user)) return null;
238
243
  const membership = (user as Record<string, unknown>).membership;
@@ -240,7 +245,12 @@ export function parseMembership(record: Record<string, unknown>): string | null
240
245
  return null;
241
246
  }
242
247
  const level = (membership as Record<string, unknown>).level;
243
- if (typeof level !== "string" || !level) return null;
248
+ return typeof level === "string" && level ? level : null;
249
+ }
250
+
251
+ export function parseMembership(record: Record<string, unknown>): string | null {
252
+ const level = parseMembershipLevel(record);
253
+ if (!level) return null;
244
254
  const name = MEMBERSHIP_LEVEL_NAMES[level];
245
255
  return name ? `Membership: ${name} (${level})` : `Membership: ${level}`;
246
256
  }
@@ -343,24 +353,42 @@ function fetchKimiUsage(token: string, signal: AbortSignal): Promise<Response> {
343
353
  });
344
354
  }
345
355
 
346
- export async function fetchKimiUsageSummary(): Promise<string> {
347
- const token = getKimiUsageToken();
348
- if (!token) return "Usage: missing credentials. Run /login kimi-coding.";
356
+ export async function fetchKimiUsageSnapshot(
357
+ options: { timeoutMs?: number; token?: string; refreshOnUnauthorized?: boolean } = {},
358
+ ): Promise<KimiUsageSnapshot> {
359
+ const token = options.token ?? getKimiUsageToken();
360
+ if (!token) {
361
+ return {
362
+ summary: "Usage: missing credentials. Run /login kimi-coding.",
363
+ membershipLevel: null,
364
+ };
365
+ }
349
366
 
350
367
  const controller = new AbortController();
351
- const timeout = setTimeout(() => controller.abort(), 15_000);
368
+ const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 15_000);
352
369
  try {
353
370
  let response = await fetchKimiUsage(token, controller.signal);
354
- if (response.status === 401) {
355
- const refreshed = await refreshKimiAuthToken(token);
371
+ if (response.status === 401 && options.refreshOnUnauthorized !== false) {
372
+ const refreshed = await refreshKimiAuthToken(token, { signal: controller.signal });
356
373
  if (refreshed) response = await fetchKimiUsage(refreshed, controller.signal);
357
374
  }
358
- if (!response.ok) return `Usage: fetch failed (${response.status})`;
359
- return parseUsageSummary(await response.json());
375
+ if (!response.ok) {
376
+ return { summary: `Usage: fetch failed (${response.status})`, membershipLevel: null };
377
+ }
378
+ const payload = (await response.json()) as unknown;
379
+ const membershipLevel =
380
+ typeof payload === "object" && payload !== null && !Array.isArray(payload)
381
+ ? parseMembershipLevel(payload as Record<string, unknown>)
382
+ : null;
383
+ return { summary: parseUsageSummary(payload), membershipLevel };
360
384
  } catch (error) {
361
385
  const message = error instanceof Error ? error.message : String(error);
362
- return `Usage: fetch failed (${message})`;
386
+ return { summary: `Usage: fetch failed (${message})`, membershipLevel: null };
363
387
  } finally {
364
388
  clearTimeout(timeout);
365
389
  }
366
390
  }
391
+
392
+ export async function fetchKimiUsageSummary(): Promise<string> {
393
+ return (await fetchKimiUsageSnapshot()).summary;
394
+ }