pi-provider-kimi-code 0.6.7 → 0.6.8

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 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.
10
+ > **Kimi K3 supported.** The provider reads Kimi's live `/v1/models` catalog as the source of truth for model availability and context limits. It refreshes the catalog at startup and through `/kimi-settings`; OAuth login and token refresh also update it.
11
11
 
12
12
  ## Why this exists
13
13
 
@@ -28,7 +28,7 @@ Kimi's API surface goes beyond the LLM itself. It includes file uploads, web sea
28
28
  - Kimi account login in Pi, plus `KIMI_API_KEY` for CI or pay-per-token use.
29
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 combined with membership-aware K3 and HighSpeed availability.
31
+ - Live model metadata from Kimi's official catalog, including server-advertised model availability and context limits.
32
32
  - OpenAI-compatible mode by default, Anthropic-compatible mode on request.
33
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.
@@ -108,15 +108,15 @@ KIMI_API_KEY=sk-... pi
108
108
 
109
109
  ## Models
110
110
 
111
- This provider publishes three Pi model IDs:
111
+ The official catalog determines the models this provider publishes. The following IDs are the fallback set when catalog discovery is unavailable:
112
112
 
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 |
113
+ | Pi model ID | Default name | Availability and context |
114
+ | --------------------------------------- | ------------------------ | ------------------------ |
115
+ | `kimi-coding/kimi-for-coding` | Kimi K2.7 Code | Official catalog |
116
+ | `kimi-coding/kimi-for-coding-highspeed` | Kimi K2.7 Code HighSpeed | Official catalog |
117
+ | `kimi-coding/k3` | Kimi K3 | Official catalog |
118
118
 
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.
119
+ Kimi's official catalog at `https://api.kimi.com/coding/v1/models` is authoritative for model availability and context windows. When discovery is unavailable, the extension uses the fallback models and defaults below.
120
120
 
121
121
  Select a model inside Pi:
122
122
 
@@ -126,7 +126,7 @@ Select a model inside Pi:
126
126
  /model kimi-coding/k3
127
127
  ```
128
128
 
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.
129
+ Kimi keeps Coding models behind aliases. Rather than hardcoding a stale model list, this extension reads the official catalog at startup, when credentials change, and through `/kimi-settings`. If your account is on a newer rollout (e.g., Kimi K2.7) or internal test, Pi can pick up the latest model IDs, names, and context sizes without waiting for a package release.
130
130
 
131
131
  Fallback values:
132
132
 
@@ -135,7 +135,7 @@ Fallback values:
135
135
  - Input: text and image
136
136
  - Reasoning: enabled
137
137
 
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.
138
+ The provider maps Pi's reasoning levels to Kimi's top-level `thinking` parameter. It sends `thinking.effort` only when the official catalog advertises the mapped value. The mapping refreshes automatically on credential refresh. Opening `/kimi-settings` also re-discovers the latest model metadata.
139
139
 
140
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.
141
141
 
package/index.ts CHANGED
@@ -42,7 +42,6 @@ import { PROVIDER_ID, PROVIDER_VERSION, getBaseUrl, getKimiApiType } from "./src
42
42
  import {
43
43
  type KimiOAuthCredentials,
44
44
  type KimiOAuthExtras,
45
- applyKimiMembershipLimitsToModel,
46
45
  buildKimiModelFromConfig,
47
46
  applyKimiOAuthExtrasToModel,
48
47
  KIMI_CODING_HIGHSPEED_MODEL_ID,
@@ -51,7 +50,7 @@ import {
51
50
  KIMI_MODEL_CATALOG_VERSION,
52
51
  discoverKimiModelMetadata,
53
52
  getKimiModelMetadata,
54
- isKimiModelAvailableForMembership,
53
+ hasKimiModelMetadata,
55
54
  resolveKimiModelConfig,
56
55
  } from "./src/models.ts";
57
56
  import { getKimiApiKey, loginKimiCode, refreshKimiCodeToken } from "./src/oauth.ts";
@@ -71,7 +70,6 @@ interface KimiRuntimeState {
71
70
  cwd: string;
72
71
  config: KimiCodeConfig;
73
72
  modelExtras: KimiOAuthExtras;
74
- membershipLevel: string | null;
75
73
  projectTrusted: boolean;
76
74
  overrides?: KimiCodeConfigPatch;
77
75
  }
@@ -167,10 +165,7 @@ async function openSettingsMenu(
167
165
  if (!modelsRefreshed && refreshedToken && refreshedToken !== modelDiscoveryToken) {
168
166
  modelsRefreshed = await refreshModelExtras(state);
169
167
  }
170
- const refreshedMembershipLevel = usageSnapshot.membershipLevel ?? state.membershipLevel;
171
- const membershipChanged = state.membershipLevel !== refreshedMembershipLevel;
172
- state.membershipLevel = refreshedMembershipLevel;
173
- if (modelsRefreshed || membershipChanged) registerKimiProvider(pi, state);
168
+ if (modelsRefreshed) registerKimiProvider(pi, state);
174
169
  const usage = usageSnapshot.summary;
175
170
 
176
171
  const projectTrusted = await isKimiProjectConfigApproved(ctx, ctx.cwd);
@@ -382,74 +377,48 @@ function saveScopeKimiCodeConfig(
382
377
  function filterAvailableKimiModels<T extends { id: string }>(
383
378
  models: T[],
384
379
  extras: KimiOAuthExtras,
385
- membershipLevel: string | null,
386
380
  ): T[] {
387
381
  const available =
388
382
  extras.modelCatalogVersion === KIMI_MODEL_CATALOG_VERSION && extras.modelCatalog
389
383
  ? new Set(Object.keys(extras.modelCatalog))
390
384
  : null;
391
- return models.filter(
392
- (model) =>
393
- (!available || available.has(model.id)) &&
394
- isKimiModelAvailableForMembership(model.id, membershipLevel) !== false,
395
- );
385
+ return models.filter((model) => !available || available.has(model.id));
396
386
  }
397
387
 
398
- function registerKimiProvider(pi: ExtensionAPI, state: KimiRuntimeState): void {
399
- const standardModel = applyKimiMembershipLimitsToModel(
400
- applyKimiOAuthExtrasToModel(
401
- buildKimiModelFromConfig(state.config.model),
402
- getKimiModelMetadata(state.modelExtras, KIMI_CODING_MODEL_ID),
403
- ),
404
- state.membershipLevel,
405
- );
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(
388
+ function catalogModelIds(extras: KimiOAuthExtras): string[] {
389
+ if (extras.modelCatalogVersion === KIMI_MODEL_CATALOG_VERSION && extras.modelCatalog) {
390
+ return Object.keys(extras.modelCatalog);
391
+ }
392
+ return [KIMI_CODING_MODEL_ID, KIMI_CODING_HIGHSPEED_MODEL_ID, KIMI_K3_MODEL_ID];
393
+ }
394
+
395
+ function buildKimiCatalogModels(state: KimiRuntimeState) {
396
+ return catalogModelIds(state.modelExtras).map((modelId) =>
414
397
  applyKimiOAuthExtrasToModel(
415
- buildKimiModelFromConfig(state.config.model, KIMI_K3_MODEL_ID),
416
- getKimiModelMetadata(state.modelExtras, KIMI_K3_MODEL_ID),
398
+ buildKimiModelFromConfig(state.config.model, modelId),
399
+ getKimiModelMetadata(state.modelExtras, modelId),
400
+ state.config.model.reasoningMap,
417
401
  ),
418
- state.membershipLevel,
419
402
  );
403
+ }
420
404
 
405
+ function registerKimiProvider(pi: ExtensionAPI, state: KimiRuntimeState): void {
421
406
  pi.registerProvider(PROVIDER_ID, {
422
407
  baseUrl: getBaseUrl(state.config.protocol),
423
408
  apiKey: "$KIMI_API_KEY",
424
409
  api: getKimiApiType(state.config.protocol),
425
410
  streamSimple: streamSimpleKimi,
426
411
 
427
- models: filterAvailableKimiModels(
428
- [standardModel, highSpeedModel, k3Model],
429
- state.modelExtras,
430
- state.membershipLevel,
431
- ),
412
+ models: filterAvailableKimiModels(buildKimiCatalogModels(state), state.modelExtras),
432
413
 
433
414
  oauth: {
434
415
  name: "Kimi Code (OAuth)",
435
416
  login: async (callbacks) => {
436
417
  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
418
  return credentials;
444
419
  },
445
420
  refreshToken: async (credentials) => {
446
421
  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
422
  return refreshed;
454
423
  },
455
424
  getApiKey: getKimiApiKey,
@@ -460,27 +429,53 @@ function registerKimiProvider(pi: ExtensionAPI, state: KimiRuntimeState): void {
460
429
  // request payload by streamSimpleKimi.
461
430
  modifyModels: (models, cred) => {
462
431
  const extras = cred as KimiOAuthCredentials;
463
- state.modelExtras = extras;
432
+ // Retain previously discovered extras when this credential carries
433
+ // none (legacy format or failed discovery): applying empty metadata
434
+ // would strip thinking levels the startup discovery already found.
435
+ if (hasKimiModelMetadata(extras)) state.modelExtras = extras;
464
436
  reloadEffectiveKimiRuntimeConfig(state, state.cwd, state.projectTrusted);
465
437
  const available =
466
438
  extras.modelCatalogVersion === KIMI_MODEL_CATALOG_VERSION && extras.modelCatalog
467
439
  ? new Set(Object.keys(extras.modelCatalog))
468
440
  : null;
469
- return models
470
- .filter(
471
- (model) =>
472
- model.provider !== PROVIDER_ID ||
473
- ((!available || available.has(model.id)) &&
474
- isKimiModelAvailableForMembership(model.id, state.membershipLevel) !== false),
475
- )
476
- .map((model) =>
477
- model.provider === PROVIDER_ID
478
- ? applyKimiMembershipLimitsToModel(
479
- applyKimiOAuthExtrasToModel(model, getKimiModelMetadata(extras, model.id)),
480
- state.membershipLevel,
481
- )
482
- : model,
441
+ if (!available) {
442
+ return [
443
+ ...models.filter((model) => model.provider !== PROVIDER_ID),
444
+ ...models
445
+ .filter((model) => model.provider === PROVIDER_ID)
446
+ .map((model) => {
447
+ const metadata = getKimiModelMetadata(extras, model.id);
448
+ if (!hasKimiModelMetadata(metadata)) return model;
449
+ return applyKimiOAuthExtrasToModel(
450
+ model,
451
+ metadata,
452
+ state.config.model.reasoningMap,
453
+ );
454
+ }),
455
+ ];
456
+ }
457
+ const catalogModels = catalogModelIds(extras).map((modelId) => {
458
+ const existing = models.find(
459
+ (model) => model.provider === PROVIDER_ID && model.id === modelId,
460
+ );
461
+ return applyKimiOAuthExtrasToModel(
462
+ existing ?? {
463
+ ...buildKimiModelFromConfig(state.config.model, modelId),
464
+ api: getKimiApiType(state.config.protocol),
465
+ provider: PROVIDER_ID,
466
+ baseUrl: getBaseUrl(state.config.protocol),
467
+ },
468
+ getKimiModelMetadata(extras, modelId),
469
+ state.config.model.reasoningMap,
483
470
  );
471
+ });
472
+ let insertedCatalog = false;
473
+ return models.flatMap((model) => {
474
+ if (model.provider !== PROVIDER_ID) return [model];
475
+ if (insertedCatalog) return [];
476
+ insertedCatalog = true;
477
+ return catalogModels;
478
+ });
484
479
  },
485
480
  },
486
481
  });
@@ -494,10 +489,9 @@ export function KimiCode(overrides?: KimiCodeConfigPatch): ExtensionFactory {
494
489
  overrides,
495
490
  );
496
491
  const discoveryToken = getKimiUsageToken();
497
- const [initialDiscovery, usageSnapshot] = await Promise.all([
498
- discoveryToken ? discoverKimiModelMetadata(discoveryToken, config.protocol) : {},
499
- fetchKimiUsageSnapshot({ timeoutMs: 2500 }),
500
- ]);
492
+ const initialDiscovery = discoveryToken
493
+ ? await discoverKimiModelMetadata(discoveryToken, config.protocol)
494
+ : {};
501
495
  let discovered = initialDiscovery;
502
496
  const refreshedToken = getKimiUsageToken();
503
497
  if (
@@ -511,7 +505,6 @@ export function KimiCode(overrides?: KimiCodeConfigPatch): ExtensionFactory {
511
505
  cwd,
512
506
  config,
513
507
  modelExtras: discovered,
514
- membershipLevel: usageSnapshot.membershipLevel,
515
508
  projectTrusted: false,
516
509
  overrides,
517
510
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-kimi-code",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "description": "Use Kimi Code in Pi without losing the Kimi-specific bits",
5
5
  "keywords": [
6
6
  "ai-provider",
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.7";
44
+ export const PROVIDER_VERSION = "0.6.8";
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
48
  // syncing with https://github.com/MoonshotAI/kimi-code.
49
- export const KIMI_UPSTREAM_VERSION = "0.26.0";
49
+ export const KIMI_UPSTREAM_VERSION = "0.28.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
@@ -1,8 +1,13 @@
1
1
  // Model identity: discovery against the server's /v1/models endpoint, plus the
2
2
  // extras-merging helpers used by both registration and the OAuth modifyModels hook.
3
3
 
4
- import type { Api, Model, OAuthCredentials } from "@earendil-works/pi-ai";
5
- import type { KimiInputModality, KimiResolvedModelConfig, ModelConfig } from "./config.ts";
4
+ import type { Api, Model, OAuthCredentials, ThinkingLevelMap } from "@earendil-works/pi-ai";
5
+ import type {
6
+ KimiInputModality,
7
+ KimiResolvedModelConfig,
8
+ ModelConfig,
9
+ ModelReasoningMap,
10
+ } from "./config.ts";
6
11
 
7
12
  import { type KimiWireProtocol, getBaseUrl } from "./constants.ts";
8
13
  import { getKimiProviderHeaders } from "./device.ts";
@@ -50,62 +55,33 @@ function mergeInputModalities(
50
55
  return (["text", "image", "video"] as const).filter((modality) => next.has(modality));
51
56
  }
52
57
 
53
- // Pricing per million tokens in USD (CNY converted at ~7.25).
54
- // Source: https://platform.kimi.com/docs/pricing/chat-k27-code
55
- const COST_STANDARD = { input: 0.897, output: 3.724, cacheRead: 0.179, cacheWrite: 0.897 };
56
- const COST_HIGH_SPEED = { input: 1.793, output: 7.448, cacheRead: 0.359, cacheWrite: 1.793 };
58
+ // Pricing per million tokens in USD.
59
+ // Sources: https://www.kimi.com/resources/kimi-k2-7-code-pricing
60
+ // https://www.kimi.com/resources/kimi-k3-pricing
61
+ const COST_STANDARD = { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0.95 };
62
+ const COST_HIGH_SPEED = { input: 1.9, output: 8, cacheRead: 0.38, cacheWrite: 1.9 };
63
+ const COST_K3 = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3 };
57
64
 
58
65
  export const KIMI_CODING_MODEL_ID = "kimi-for-coding";
59
66
  export const KIMI_CODING_HIGHSPEED_MODEL_ID = "kimi-for-coding-highspeed";
60
67
  export const KIMI_K3_MODEL_ID = "k3";
61
68
  export const KIMI_MODEL_CATALOG_VERSION = 1;
62
69
 
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(
70
+ function resolveModelCost(
76
71
  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
- }
100
-
101
- function resolveModelCost(modelDisplay: string | undefined): {
72
+ modelDisplay?: string,
73
+ ): {
102
74
  input: number;
103
75
  output: number;
104
76
  cacheRead: number;
105
77
  cacheWrite: number;
106
78
  } {
107
- if (modelDisplay && /high\s*speed/i.test(modelDisplay)) return COST_HIGH_SPEED;
108
- return COST_STANDARD;
79
+ if (modelId === KIMI_K3_MODEL_ID) return COST_K3;
80
+ if (modelId === KIMI_CODING_MODEL_ID) return COST_STANDARD;
81
+ if (modelId === KIMI_CODING_HIGHSPEED_MODEL_ID || /high\s*speed/i.test(modelDisplay ?? "")) {
82
+ return COST_HIGH_SPEED;
83
+ }
84
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
109
85
  }
110
86
 
111
87
  export function buildKimiModelFromConfig(
@@ -118,13 +94,15 @@ export function buildKimiModelFromConfig(
118
94
  ? "Kimi K3"
119
95
  : isHighSpeed
120
96
  ? "Kimi for Coding High Speed"
121
- : "Kimi for Coding";
97
+ : modelId === KIMI_CODING_MODEL_ID
98
+ ? "Kimi for Coding"
99
+ : modelId;
122
100
  return {
123
101
  id: modelId,
124
102
  name,
125
103
  reasoning: config.reasoning,
126
104
  input: [...config.input] as unknown as ("text" | "image" | "video")[],
127
- cost: { ...(isHighSpeed ? COST_HIGH_SPEED : COST_STANDARD) },
105
+ cost: { ...resolveModelCost(modelId) },
128
106
  contextWindow: config.contextWindow,
129
107
  maxTokens: config.maxTokens,
130
108
  } as Model<Api>;
@@ -226,18 +204,29 @@ function getModelsUrl(protocol?: KimiWireProtocol): string {
226
204
  return buildModelsUrl(getBaseUrl(protocol));
227
205
  }
228
206
 
207
+ export function isOfficialKimiModelsUrl(url: string): boolean {
208
+ try {
209
+ const parsed = new URL(url);
210
+ return parsed.origin === "https://api.kimi.com" && parsed.pathname === "/coding/v1/models";
211
+ } catch {
212
+ return false;
213
+ }
214
+ }
215
+
229
216
  export async function discoverKimiModelMetadata(
230
217
  accessToken: string,
231
218
  protocol?: KimiWireProtocol,
232
219
  options: DiscoverKimiModelMetadataOptions = {},
233
220
  ): Promise<KimiOAuthExtras> {
234
221
  if (!accessToken) return {};
222
+ const modelsUrl = getModelsUrl(protocol);
223
+ if (!isOfficialKimiModelsUrl(modelsUrl)) return {};
235
224
  const timeoutMs = options.timeoutMs ?? DEFAULT_DISCOVERY_TIMEOUT_MS;
236
225
  const controller = new AbortController();
237
226
  const timeout =
238
227
  timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs).unref() : undefined;
239
228
  try {
240
- const response = await fetch(getModelsUrl(protocol), {
229
+ const response = await fetch(modelsUrl, {
241
230
  signal: controller.signal,
242
231
  headers: {
243
232
  ...getKimiProviderHeaders(),
@@ -248,16 +237,10 @@ export async function discoverKimiModelMetadata(
248
237
  if (!response.ok) return {};
249
238
  const json = (await response.json()) as { data?: unknown };
250
239
  const list = Array.isArray(json.data) ? (json.data as KimiServerModel[]) : [];
251
- const supportedIds = new Set([
252
- KIMI_CODING_MODEL_ID,
253
- KIMI_CODING_HIGHSPEED_MODEL_ID,
254
- KIMI_K3_MODEL_ID,
255
- ]);
256
240
  const modelCatalog: Record<string, KimiModelMetadata> = {};
257
241
  for (const model of list) {
258
- if (typeof model.id !== "string" || !supportedIds.has(model.id)) continue;
259
242
  const metadata = parseKimiModelMetadata(model);
260
- if (metadata) modelCatalog[model.id] = metadata;
243
+ if (metadata) modelCatalog[metadata.wireModelId!] = metadata;
261
244
  }
262
245
  const preferred = modelCatalog[KIMI_CODING_MODEL_ID];
263
246
  if (!preferred) {
@@ -279,9 +262,49 @@ export function getKimiModelMetadata(extras: KimiOAuthExtras, modelId: string):
279
262
  return modelId === KIMI_CODING_MODEL_ID ? extras : {};
280
263
  }
281
264
 
265
+ const KIMI_DISCOVERY_KEYS = [
266
+ "wireModelId",
267
+ "modelDisplay",
268
+ "contextLength",
269
+ "supportsReasoning",
270
+ "supportsImageIn",
271
+ "supportsVideoIn",
272
+ "supportsThinkingType",
273
+ "protocol",
274
+ "supportEfforts",
275
+ "defaultEffort",
276
+ "modelCatalog",
277
+ ] as const;
278
+
279
+ // Credentials always carry access/refresh/expires, so discovery presence must
280
+ // be checked on the metadata fields themselves. Empty means either a legacy
281
+ // pre-discovery credential or a failed discovery during login/refresh.
282
+ export function hasKimiModelMetadata(extras: KimiOAuthExtras): boolean {
283
+ return KIMI_DISCOVERY_KEYS.some((key) => extras[key] !== undefined);
284
+ }
285
+
286
+ export function buildKimiThinkingLevelMap(
287
+ reasoningMap: ModelReasoningMap,
288
+ extras: Pick<KimiModelMetadata, "supportEfforts" | "supportsThinkingType">,
289
+ ): ThinkingLevelMap | undefined {
290
+ if (!extras.supportEfforts?.length) return undefined;
291
+ const map = {
292
+ off: extras.supportsThinkingType === "only" ? null : "off",
293
+ } as ThinkingLevelMap & Record<string, string | null>;
294
+ for (const level of ["minimal", "low", "medium", "high", "xhigh", "max"] as const) {
295
+ const entry = reasoningMap[level];
296
+ map[level] =
297
+ entry?.enabled && entry.effort && extras.supportEfforts.includes(entry.effort)
298
+ ? entry.effort
299
+ : null;
300
+ }
301
+ return map;
302
+ }
303
+
282
304
  export function applyKimiOAuthExtrasToModel(
283
305
  model: Model<Api>,
284
306
  extras: KimiModelMetadata,
307
+ reasoningMap?: ModelReasoningMap,
285
308
  ): Model<Api> {
286
309
  const next: Model<Api> & {
287
310
  wireModelId?: string;
@@ -295,7 +318,7 @@ export function applyKimiOAuthExtrasToModel(
295
318
  model.id === KIMI_K3_MODEL_ID && /^k3$/i.test(extras.modelDisplay)
296
319
  ? "Kimi K3"
297
320
  : extras.modelDisplay;
298
- next.cost = resolveModelCost(extras.modelDisplay);
321
+ next.cost = resolveModelCost(model.id, extras.modelDisplay);
299
322
  }
300
323
  if (typeof extras.contextLength === "number" && extras.contextLength > 0) {
301
324
  next.contextWindow = extras.contextLength;
@@ -316,6 +339,13 @@ export function applyKimiOAuthExtrasToModel(
316
339
  }
317
340
  if (extras.protocol) next.wireProtocol = extras.protocol;
318
341
  if (extras.supportEfforts) next.supportEfforts = [...extras.supportEfforts];
342
+ else delete next.supportEfforts;
319
343
  if (extras.defaultEffort) next.defaultEffort = extras.defaultEffort;
344
+ else delete next.defaultEffort;
345
+ if (reasoningMap) {
346
+ const thinkingLevelMap = buildKimiThinkingLevelMap(reasoningMap, extras);
347
+ if (thinkingLevelMap) next.thinkingLevelMap = thinkingLevelMap;
348
+ else delete next.thinkingLevelMap;
349
+ }
320
350
  return next;
321
351
  }
package/src/oauth.ts CHANGED
@@ -52,43 +52,107 @@ export function readStoredOAuthCredential(providerId: string): StoredOAuthCreden
52
52
  // while held, recovers stale locks from crashed processes, and retries for
53
53
  // ~40s so a live holder running a slow network refresh inside the lock is
54
54
  // waited out.
55
- async function acquireAuthFileLock(authPath: string): Promise<() => Promise<void>> {
56
- return acquireFileLock(authPath, {
57
- realpath: false,
55
+ async function acquireSharedFileLock(
56
+ path: string,
57
+ signal: AbortSignal | undefined,
58
+ options: { maxRetries: number; stale: number; minTimeout: number; maxTimeout: number },
59
+ ): Promise<() => Promise<void>> {
60
+ for (let attempt = 0; ; attempt++) {
61
+ signal?.throwIfAborted();
62
+ try {
63
+ return await acquireFileLock(path, {
64
+ realpath: false,
65
+ stale: options.stale,
66
+ // Retry via the outer abortable sleep below. proper-lockfile's own
67
+ // retry loop is not signal-aware and would otherwise ignore an abort
68
+ // for the full configured retry window.
69
+ retries: 0,
70
+ });
71
+ } catch (error) {
72
+ const code =
73
+ typeof error === "object" && error !== null && "code" in error
74
+ ? String((error as { code?: unknown }).code)
75
+ : undefined;
76
+ if (code !== "ELOCKED" || attempt >= options.maxRetries) throw error;
77
+ const baseDelay = Math.min(options.minTimeout * 2 ** attempt, options.maxTimeout);
78
+ await sleep(baseDelay * (1 + Math.random()), signal);
79
+ }
80
+ }
81
+ }
82
+
83
+ async function acquireAuthFileLock(
84
+ authPath: string,
85
+ signal?: AbortSignal,
86
+ ): Promise<() => Promise<void>> {
87
+ return acquireSharedFileLock(authPath, signal, {
88
+ maxRetries: 10,
58
89
  stale: 30_000,
59
- retries: { retries: 10, factor: 2, minTimeout: 100, maxTimeout: 10_000, randomize: true },
90
+ minTimeout: 100,
91
+ maxTimeout: 10_000,
60
92
  });
61
93
  }
62
94
 
63
- async function writeStoredCredential(
64
- providerId: string,
65
- credential: StoredOAuthCredential,
66
- ): Promise<void> {
67
- const AuthStorage = piAgentRuntime.AuthStorage;
68
- if (AuthStorage) {
69
- AuthStorage.create().set(providerId, credential);
70
- return;
95
+ async function acquireKimiCodeRefreshLock(signal?: AbortSignal): Promise<() => Promise<void>> {
96
+ if (process.platform === "win32" || process.env.KIMI_DISABLE_OAUTH_LOCK === "1") {
97
+ return async () => {};
71
98
  }
72
- // pi >=0.80.8: no exported write path, so do a locked read-modify-write of
73
- // auth.json in pi's on-disk format (indent 2). The read happens inside the
74
- // lock so concurrent updates from other processes are never clobbered, and
75
- // the explicit chmod covers pre-existing files (writeFileSync's mode only
76
- // applies on creation).
99
+ const target = join(
100
+ process.env.KIMI_CODE_HOME || join(os.homedir(), ".kimi-code"),
101
+ "oauth",
102
+ "kimi-code",
103
+ );
104
+ mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
105
+ writeFileSync(target, "", { encoding: "utf-8", flag: "a" });
106
+ return acquireSharedFileLock(target, signal, {
107
+ maxRetries: 120,
108
+ stale: 5_000,
109
+ minTimeout: 500,
110
+ maxTimeout: 1_000,
111
+ });
112
+ }
113
+
114
+ interface LockedOAuthCredential {
115
+ credential: StoredOAuthCredential | null;
116
+ store(credential: StoredOAuthCredential): void;
117
+ release(): Promise<void>;
118
+ }
119
+
120
+ async function lockStoredOAuthCredential(
121
+ providerId: string,
122
+ signal?: AbortSignal,
123
+ ): Promise<LockedOAuthCredential> {
124
+ // Hold the same lock Pi uses across the complete read-refresh-write
125
+ // transaction. Kimi rotates refresh tokens, so locking only the write lets
126
+ // concurrent Pi processes exchange the same token twice.
77
127
  const authPath = join(piAgentRuntime.getAgentDir(), "auth.json");
78
- const release = await acquireAuthFileLock(authPath);
128
+ mkdirSync(dirname(authPath), { recursive: true, mode: 0o700 });
129
+ if (!existsSync(authPath)) {
130
+ writeFileSync(authPath, "{}", { encoding: "utf-8", mode: 0o600 });
131
+ chmodSync(authPath, 0o600);
132
+ }
133
+ const release = await acquireAuthFileLock(authPath, signal);
134
+ let data: Record<string, unknown> = {};
79
135
  try {
80
- let data: Record<string, unknown> = {};
81
- try {
82
- data = JSON.parse(readFileSync(authPath, "utf-8"));
83
- } catch {
84
- // Missing or unreadable auth.json — start from an empty store.
136
+ const parsed = JSON.parse(readFileSync(authPath, "utf-8")) as unknown;
137
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
138
+ data = parsed as Record<string, unknown>;
85
139
  }
86
- data[providerId] = credential;
87
- writeFileSync(authPath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 0o600 });
88
- chmodSync(authPath, 0o600);
89
- } finally {
90
- await release();
140
+ } catch {
141
+ // Missing or unreadable auth.json — treat it as an empty store.
91
142
  }
143
+ const stored = data[providerId] as StoredCredential | undefined;
144
+ return {
145
+ credential: stored?.type === "oauth" ? (stored as unknown as StoredOAuthCredential) : null,
146
+ store(credential) {
147
+ data[providerId] = credential;
148
+ writeFileSync(authPath, JSON.stringify(data, null, 2), {
149
+ encoding: "utf-8",
150
+ mode: 0o600,
151
+ });
152
+ chmodSync(authPath, 0o600);
153
+ },
154
+ release,
155
+ };
92
156
  }
93
157
 
94
158
  import { CLIENT_ID, PROVIDER_ID, RETRYABLE_REFRESH_STATUSES, getOAuthHost } from "./constants.ts";
@@ -339,14 +403,10 @@ function readKimiCliCredentials(): KimiCliCredentialsFile | null {
339
403
  return null;
340
404
  }
341
405
 
342
- function kimiCodeCredentialExists(): boolean {
343
- return existsSync(getKimiCodeCredentialPath());
344
- }
345
-
346
406
  function writeKimiCodeCredentials(access: string, refresh: string, expiresMs: number): void {
347
407
  const path = getKimiCodeCredentialPath();
348
408
  const dir = dirname(path);
349
- mkdirSync(dir, { recursive: true });
409
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
350
410
  const data: KimiCliCredentialsFile = {
351
411
  access_token: access,
352
412
  refresh_token: refresh,
@@ -355,7 +415,24 @@ function writeKimiCodeCredentials(access: string, refresh: string, expiresMs: nu
355
415
  token_type: "Bearer",
356
416
  expires_in: Math.max(0, Math.floor((expiresMs - Date.now()) / 1000)),
357
417
  };
358
- writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
418
+ writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
419
+ chmodSync(path, 0o600);
420
+ }
421
+
422
+ interface LockedKimiCodeCredentials {
423
+ credential: KimiCliCredentialsFile | null;
424
+ store(access: string, refresh: string, expiresMs: number): void;
425
+ release(): Promise<void>;
426
+ }
427
+
428
+ async function lockKimiCodeCredentials(signal?: AbortSignal): Promise<LockedKimiCodeCredentials> {
429
+ const release = await acquireKimiCodeRefreshLock(signal);
430
+ const credential = readKimiCliCredentials();
431
+ return {
432
+ credential,
433
+ store: writeKimiCodeCredentials,
434
+ release,
435
+ };
359
436
  }
360
437
 
361
438
  async function tryReuseKimiCliCredentials(
@@ -476,26 +553,83 @@ export async function loginKimiCode(callbacks: OAuthLoginCallbacks): Promise<Kim
476
553
  export async function refreshKimiCodeToken(
477
554
  credentials: OAuthCredentials,
478
555
  ): Promise<KimiOAuthCredentials> {
479
- const kimiCred = kimiCodeCredentialExists() ? readKimiCliCredentials() : null;
480
- let token: Awaited<ReturnType<typeof refreshAccessToken>>;
556
+ const kimiLocked = await lockKimiCodeCredentials();
481
557
  try {
482
- token = await refreshAccessToken(credentials.refresh);
483
- } catch (error) {
484
- if (kimiCred?.refresh_token && kimiCred.refresh_token !== credentials.refresh) {
485
- token = await refreshAccessToken(kimiCred.refresh_token);
486
- } else {
558
+ const initialKimiCred = kimiLocked.credential;
559
+ const initialKimiExpiresMs = (initialKimiCred?.expires_at ?? 0) * 1000;
560
+ const peerHasNewerValidAccess =
561
+ initialKimiCred?.access_token !== undefined &&
562
+ initialKimiCred.access_token !== credentials.access &&
563
+ Date.now() < initialKimiExpiresMs;
564
+ const activeRefreshToken = peerHasNewerValidAccess
565
+ ? initialKimiCred.refresh_token!
566
+ : credentials.refresh;
567
+ const activeAccessToken = peerHasNewerValidAccess
568
+ ? initialKimiCred.access_token!
569
+ : credentials.access;
570
+ if (peerHasNewerValidAccess && initialKimiCred.refresh_token !== credentials.refresh) {
571
+ const extras = await discoverKimiModelMetadata(activeAccessToken);
572
+ return {
573
+ access: activeAccessToken,
574
+ refresh: activeRefreshToken,
575
+ expires: initialKimiExpiresMs,
576
+ ...extras,
577
+ };
578
+ }
579
+ let token: Awaited<ReturnType<typeof refreshAccessToken>>;
580
+ try {
581
+ token = await refreshAccessToken(activeRefreshToken);
582
+ } catch (error) {
583
+ // Lock-disabled platforms (Windows / KIMI_DISABLE_OAUTH_LOCK=1) use
584
+ // storage re-read as the only coordinator. A peer may have rotated the
585
+ // shared sidecar while this process was exchanging the old refresh
586
+ // token; recover from that newer valid credential instead of surfacing
587
+ // the stale-token invalid_grant as a login failure.
588
+ await sleep(100);
589
+ const peerCredential = readKimiCliCredentials();
590
+ const peerAccess = peerCredential?.access_token;
591
+ const peerRefresh = peerCredential?.refresh_token;
592
+ const peerExpiresMs = (peerCredential?.expires_at ?? 0) * 1000;
593
+ if (
594
+ peerAccess !== undefined &&
595
+ peerRefresh !== undefined &&
596
+ peerAccess !== credentials.access &&
597
+ Date.now() < peerExpiresMs
598
+ ) {
599
+ const extras = await discoverKimiModelMetadata(peerAccess);
600
+ return {
601
+ access: peerAccess,
602
+ refresh: peerRefresh,
603
+ expires: peerExpiresMs,
604
+ ...extras,
605
+ };
606
+ }
487
607
  throw error;
488
608
  }
609
+ const expiresMs = Date.now() + token.expires_in * 1000;
610
+ const kimiCredentialAfterRefresh = readKimiCliCredentials();
611
+ const peerAccess = kimiCredentialAfterRefresh?.access_token;
612
+ const peerRefresh = kimiCredentialAfterRefresh?.refresh_token;
613
+ const peerExpiresMs = (kimiCredentialAfterRefresh?.expires_at ?? 0) * 1000;
614
+ const accessToken =
615
+ peerAccess !== undefined && peerAccess !== activeAccessToken && Date.now() < peerExpiresMs
616
+ ? peerAccess
617
+ : token.access_token;
618
+ const refreshToken = accessToken === peerAccess ? peerRefresh! : token.refresh_token;
619
+ const effectiveExpiresMs = accessToken === peerAccess ? peerExpiresMs : expiresMs;
620
+ if (accessToken !== peerAccess) {
621
+ kimiLocked.store(accessToken, refreshToken, effectiveExpiresMs);
622
+ }
623
+ const extras = await discoverKimiModelMetadata(accessToken);
624
+ return {
625
+ access: accessToken,
626
+ refresh: refreshToken,
627
+ expires: effectiveExpiresMs,
628
+ ...extras,
629
+ };
630
+ } finally {
631
+ await kimiLocked.release().catch(() => {});
489
632
  }
490
- const expiresMs = Date.now() + token.expires_in * 1000;
491
- writeKimiCodeCredentials(token.access_token, token.refresh_token, expiresMs);
492
- const extras = await discoverKimiModelMetadata(token.access_token);
493
- return {
494
- access: token.access_token,
495
- refresh: token.refresh_token,
496
- expires: expiresMs,
497
- ...extras,
498
- };
499
633
  }
500
634
 
501
635
  // =============================================================================
@@ -532,21 +666,23 @@ export async function refreshKimiAuthToken(
532
666
  currentKey: string,
533
667
  options: { signal?: AbortSignal } = {},
534
668
  ): Promise<string | null> {
669
+ let locked: LockedOAuthCredential | undefined;
670
+ let kimiLocked: LockedKimiCodeCredentials | undefined;
535
671
  try {
536
- const kimiCred = readKimiCliCredentials();
537
- const piOAuth = readStoredOAuthCredential(PROVIDER_ID);
672
+ locked = await lockStoredOAuthCredential(PROVIDER_ID, options.signal);
673
+ const piOAuth = locked.credential;
538
674
 
539
675
  if (piOAuth) {
540
676
  if (piOAuth.access !== currentKey && Date.now() < piOAuth.expires) {
541
- console.error("[kimi-coding] auth refresh: reusing newer on-disk token");
542
677
  return piOAuth.access;
543
678
  }
544
679
 
545
- console.error("[kimi-coding] auth refresh: requesting new access token");
546
680
  let refreshed: Awaited<ReturnType<typeof refreshAccessToken>>;
681
+ kimiLocked = await lockKimiCodeCredentials(options.signal);
547
682
  try {
548
683
  refreshed = await refreshAccessToken(piOAuth.refresh, { signal: options.signal });
549
684
  } catch (error) {
685
+ const kimiCred = readKimiCliCredentials();
550
686
  if (!kimiCred?.refresh_token || kimiCred.refresh_token === piOAuth.refresh) throw error;
551
687
  const kimiExpiresMs = (kimiCred.expires_at ?? 0) * 1000;
552
688
  if (kimiCred.access_token !== currentKey && Date.now() < kimiExpiresMs) {
@@ -557,15 +693,30 @@ export async function refreshKimiAuthToken(
557
693
  refresh: kimiCred.refresh_token,
558
694
  expires: kimiExpiresMs,
559
695
  };
560
- await writeStoredCredential(PROVIDER_ID, recovered);
561
- console.error("[kimi-coding] auth refresh: recovered newer kimi-code token");
696
+ locked.store(recovered);
562
697
  return recovered.access;
563
698
  }
564
- console.error("[kimi-coding] auth refresh: pi token rejected, trying kimi-code token");
565
699
  refreshed = await refreshAccessToken(kimiCred.refresh_token, { signal: options.signal });
566
700
  }
567
701
 
568
702
  const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
703
+ const kimiCredentialAfterRefresh = readKimiCliCredentials();
704
+ const peerAccess = kimiCredentialAfterRefresh?.access_token;
705
+ const peerExpiresMs = (kimiCredentialAfterRefresh?.expires_at ?? 0) * 1000;
706
+ const peerIsNewerValid =
707
+ peerAccess !== undefined && peerAccess !== currentKey && Date.now() < peerExpiresMs;
708
+ if (peerIsNewerValid) {
709
+ const peerCred: StoredOAuthCredential = {
710
+ ...piOAuth,
711
+ type: "oauth",
712
+ access: peerAccess,
713
+ refresh: kimiCredentialAfterRefresh!.refresh_token!,
714
+ expires: peerExpiresMs,
715
+ };
716
+ locked.store(peerCred);
717
+ return peerCred.access;
718
+ }
719
+ kimiLocked.store(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
569
720
  const newCred: StoredOAuthCredential = {
570
721
  ...piOAuth,
571
722
  type: "oauth",
@@ -573,32 +724,32 @@ export async function refreshKimiAuthToken(
573
724
  refresh: refreshed.refresh_token,
574
725
  expires: newExpiresMs,
575
726
  };
576
- await writeStoredCredential(PROVIDER_ID, newCred);
577
- writeKimiCodeCredentials(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
578
- console.error("[kimi-coding] auth refresh: new token persisted");
727
+ locked.store(newCred);
579
728
  return newCred.access;
580
729
  }
581
730
 
731
+ kimiLocked = await lockKimiCodeCredentials(options.signal);
732
+ const kimiCred = kimiLocked.credential;
582
733
  if (!kimiCred) {
583
- console.error(
584
- `[kimi-coding] auth refresh skipped: no OAuth credentials for ${PROVIDER_ID} on disk`,
585
- );
586
734
  return null;
587
735
  }
588
736
 
589
737
  const kimiExpiresMs = (kimiCred.expires_at ?? 0) * 1000;
590
738
  if (kimiCred.access_token !== currentKey && Date.now() < kimiExpiresMs) {
591
- console.error("[kimi-coding] auth refresh: reusing newer kimi-code token");
592
739
  return kimiCred.access_token!;
593
740
  }
594
741
 
595
- console.error("[kimi-coding] auth refresh: requesting new access token via kimi-code");
596
742
  const refreshed = await refreshAccessToken(kimiCred.refresh_token!, {
597
743
  signal: options.signal,
598
744
  });
599
745
  const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
600
- writeKimiCodeCredentials(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
601
- console.error("[kimi-coding] auth refresh: new token persisted to kimi-code");
746
+ const kimiCredentialAfterRefresh = readKimiCliCredentials();
747
+ const peerAccess = kimiCredentialAfterRefresh?.access_token;
748
+ const peerExpiresMs = (kimiCredentialAfterRefresh?.expires_at ?? 0) * 1000;
749
+ if (peerAccess !== undefined && peerAccess !== currentKey && Date.now() < peerExpiresMs) {
750
+ return peerAccess;
751
+ }
752
+ kimiLocked.store(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
602
753
  return refreshed.access_token;
603
754
  } catch (err) {
604
755
  if (options.signal?.aborted) return null;
@@ -608,5 +759,12 @@ export async function refreshKimiAuthToken(
608
759
  console.error("[kimi-coding] auth refresh failed:", err);
609
760
  }
610
761
  return null;
762
+ } finally {
763
+ if (kimiLocked) {
764
+ await kimiLocked.release().catch(() => {});
765
+ }
766
+ if (locked) {
767
+ await locked.release().catch(() => {});
768
+ }
611
769
  }
612
770
  }
package/src/usage.ts CHANGED
@@ -2,15 +2,6 @@ import { PROVIDER_ID, getBaseUrl } from "./constants.ts";
2
2
  import { getKimiProviderHeaders } from "./device.ts";
3
3
  import { readStoredOAuthCredential, refreshKimiAuthToken } from "./oauth.ts";
4
4
 
5
- const MEMBERSHIP_LEVEL_NAMES: Record<string, string> = {
6
- LEVEL_FREE: "Free",
7
- LEVEL_BASIC: "Adagio",
8
- LEVEL_STANDARD: "Moderato",
9
- LEVEL_INTERMEDIATE: "Allegretto",
10
- LEVEL_ADVANCED: "Allegro",
11
- LEVEL_PREMIUM: "Vivace",
12
- };
13
-
14
5
  const RESET_TIME_KEYS = [
15
6
  "resetTime",
16
7
  "reset_time",
@@ -36,7 +27,6 @@ export interface UsageFormatOptions {
36
27
 
37
28
  export interface KimiUsageSnapshot {
38
29
  summary: string;
39
- membershipLevel: string | null;
40
30
  }
41
31
 
42
32
  interface BoosterWalletInfo {
@@ -174,9 +164,6 @@ export function parseUsageSummary(payload: unknown, options: UsageFormatOptions
174
164
 
175
165
  const record = payload as Record<string, unknown>;
176
166
  const lines: string[] = [];
177
- const membership = parseMembership(record);
178
- if (membership) lines.push(membership, "");
179
-
180
167
  const summary = parseUsageRow(record.usage, "Current week");
181
168
  if (summary)
182
169
  lines.push(formatUsageRow({ ...summary, label: normalizeUsageLabel(summary.label) }, options));
@@ -235,24 +222,6 @@ function formatWindowLabel(value: unknown, fallbackLabel: string): string {
235
222
  return `Current ${minutes}m window`;
236
223
  }
237
224
 
238
- export function parseMembershipLevel(record: Record<string, unknown>): string | null {
239
- const user = record.user;
240
- if (typeof user !== "object" || user === null || Array.isArray(user)) return null;
241
- const membership = (user as Record<string, unknown>).membership;
242
- if (typeof membership !== "object" || membership === null || Array.isArray(membership)) {
243
- return null;
244
- }
245
- const level = (membership as Record<string, unknown>).level;
246
- return typeof level === "string" && level ? level : null;
247
- }
248
-
249
- export function parseMembership(record: Record<string, unknown>): string | null {
250
- const level = parseMembershipLevel(record);
251
- if (!level) return null;
252
- const name = MEMBERSHIP_LEVEL_NAMES[level];
253
- return name ? `Membership: ${name} (${level})` : `Membership: ${level}`;
254
- }
255
-
256
225
  export function formatUsageRow(row: UsageRow, options: UsageFormatOptions = {}): string {
257
226
  if (row.limit <= 0) return `${row.label}\n${row.used} used`;
258
227
  const used = Math.max(0, Math.min(row.used, row.limit));
@@ -356,10 +325,7 @@ export async function fetchKimiUsageSnapshot(
356
325
  ): Promise<KimiUsageSnapshot> {
357
326
  const token = options.token ?? getKimiUsageToken();
358
327
  if (!token) {
359
- return {
360
- summary: "Usage: missing credentials. Run /login kimi-coding.",
361
- membershipLevel: null,
362
- };
328
+ return { summary: "Usage: missing credentials. Run /login kimi-coding." };
363
329
  }
364
330
 
365
331
  const controller = new AbortController();
@@ -371,17 +337,13 @@ export async function fetchKimiUsageSnapshot(
371
337
  if (refreshed) response = await fetchKimiUsage(refreshed, controller.signal);
372
338
  }
373
339
  if (!response.ok) {
374
- return { summary: `Usage: fetch failed (${response.status})`, membershipLevel: null };
340
+ return { summary: `Usage: fetch failed (${response.status})` };
375
341
  }
376
342
  const payload = (await response.json()) as unknown;
377
- const membershipLevel =
378
- typeof payload === "object" && payload !== null && !Array.isArray(payload)
379
- ? parseMembershipLevel(payload as Record<string, unknown>)
380
- : null;
381
- return { summary: parseUsageSummary(payload), membershipLevel };
343
+ return { summary: parseUsageSummary(payload) };
382
344
  } catch (error) {
383
345
  const message = error instanceof Error ? error.message : String(error);
384
- return { summary: `Usage: fetch failed (${message})`, membershipLevel: null };
346
+ return { summary: `Usage: fetch failed (${message})` };
385
347
  } finally {
386
348
  clearTimeout(timeout);
387
349
  }