pi-provider-kimi-code 0.6.6 → 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 +11 -11
- package/index.ts +62 -69
- package/package.json +8 -4
- package/src/constants.ts +2 -2
- package/src/models.ts +87 -57
- package/src/oauth.ts +285 -48
- package/src/stream.ts +53 -7
- package/src/tools/common.ts +7 -5
- package/src/usage.ts +7 -47
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
|
|
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
|
|
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
|
-
|
|
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 |
|
|
114
|
-
| --------------------------------------- | ------------------------ |
|
|
115
|
-
| `kimi-coding/kimi-for-coding` | Kimi K2.7 Code |
|
|
116
|
-
| `kimi-coding/kimi-for-coding-highspeed` | Kimi K2.7 Code HighSpeed |
|
|
117
|
-
| `kimi-coding/k3` | Kimi K3 |
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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,
|
|
416
|
-
getKimiModelMetadata(state.modelExtras,
|
|
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
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
(model) =>
|
|
472
|
-
|
|
473
|
-
((
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
state.
|
|
481
|
-
)
|
|
482
|
-
|
|
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
|
|
498
|
-
|
|
499
|
-
|
|
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.
|
|
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",
|
|
@@ -49,12 +49,16 @@
|
|
|
49
49
|
"format:check": "oxfmt . --check",
|
|
50
50
|
"pre-commit": "prek run"
|
|
51
51
|
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"proper-lockfile": "^4.1.2"
|
|
54
|
+
},
|
|
52
55
|
"devDependencies": {
|
|
53
|
-
"@earendil-works/pi-ai": "^0.
|
|
54
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
55
|
-
"@earendil-works/pi-tui": "^0.
|
|
56
|
+
"@earendil-works/pi-ai": "^0.80.9",
|
|
57
|
+
"@earendil-works/pi-coding-agent": "^0.80.9",
|
|
58
|
+
"@earendil-works/pi-tui": "^0.80.9",
|
|
56
59
|
"@j178/prek": "^0.4.4",
|
|
57
60
|
"@types/node": "^24.0.0",
|
|
61
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
58
62
|
"oxfmt": "^0.44.0",
|
|
59
63
|
"oxlint": "^1.59.0",
|
|
60
64
|
"typescript": "^5.9.0"
|
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.
|
|
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.
|
|
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 {
|
|
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
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
):
|
|
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 (
|
|
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
|
-
:
|
|
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: { ...(
|
|
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(
|
|
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[
|
|
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
|
@@ -2,13 +2,158 @@
|
|
|
2
2
|
// reuse, the login / refresh handlers wired into pi's OAuth interface, and
|
|
3
3
|
// the stream-level auth refresh used by the streaming handler.
|
|
4
4
|
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
|
|
9
|
+
import { lock as acquireFileLock } from "proper-lockfile";
|
|
10
|
+
|
|
9
11
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
+
import * as piAgent from "@earendil-works/pi-coding-agent";
|
|
13
|
+
|
|
14
|
+
// Structural mirrors of pi's stored credential shapes. The exported names
|
|
15
|
+
// moved between pi versions (pi-coding-agent <=0.79 vs pi-ai >=0.80.8), so
|
|
16
|
+
// these are defined locally to type-check against either surface.
|
|
17
|
+
type StoredCredential = { type: string } & Record<string, unknown>;
|
|
18
|
+
|
|
19
|
+
export interface StoredOAuthCredential {
|
|
20
|
+
type: "oauth";
|
|
21
|
+
access: string;
|
|
22
|
+
refresh: string;
|
|
23
|
+
expires: number;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// pi renamed its auth storage surface in 0.80.8: the exported AuthStorage
|
|
28
|
+
// class (sync get/set under a file lock) was replaced by readStoredCredential
|
|
29
|
+
// for one-off reads, with no exported write path. Detect whichever the host
|
|
30
|
+
// pi provides so the extension keeps loading on both sides of the rename.
|
|
31
|
+
const piAgentRuntime = piAgent as unknown as {
|
|
32
|
+
getAgentDir(): string;
|
|
33
|
+
readStoredCredential?(providerId: string): StoredCredential | undefined;
|
|
34
|
+
AuthStorage?: {
|
|
35
|
+
create(): {
|
|
36
|
+
get(provider: string): StoredCredential | undefined;
|
|
37
|
+
set(provider: string, credential: StoredCredential): void;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function readStoredOAuthCredential(providerId: string): StoredOAuthCredential | null {
|
|
43
|
+
const credential = piAgentRuntime.readStoredCredential
|
|
44
|
+
? piAgentRuntime.readStoredCredential(providerId)
|
|
45
|
+
: piAgentRuntime.AuthStorage?.create().get(providerId);
|
|
46
|
+
return credential?.type === "oauth" ? (credential as StoredOAuthCredential) : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Mutual exclusion with pi's own credential writes: use the same library
|
|
50
|
+
// (proper-lockfile) on the same file pi's FileAuthStorageBackend locks, with
|
|
51
|
+
// the same options. The shared implementation keeps the lock mtime fresh
|
|
52
|
+
// while held, recovers stale locks from crashed processes, and retries for
|
|
53
|
+
// ~40s so a live holder running a slow network refresh inside the lock is
|
|
54
|
+
// waited out.
|
|
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,
|
|
89
|
+
stale: 30_000,
|
|
90
|
+
minTimeout: 100,
|
|
91
|
+
maxTimeout: 10_000,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function acquireKimiCodeRefreshLock(signal?: AbortSignal): Promise<() => Promise<void>> {
|
|
96
|
+
if (process.platform === "win32" || process.env.KIMI_DISABLE_OAUTH_LOCK === "1") {
|
|
97
|
+
return async () => {};
|
|
98
|
+
}
|
|
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.
|
|
127
|
+
const authPath = join(piAgentRuntime.getAgentDir(), "auth.json");
|
|
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> = {};
|
|
135
|
+
try {
|
|
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>;
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// Missing or unreadable auth.json — treat it as an empty store.
|
|
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
|
+
};
|
|
156
|
+
}
|
|
12
157
|
|
|
13
158
|
import { CLIENT_ID, PROVIDER_ID, RETRYABLE_REFRESH_STATUSES, getOAuthHost } from "./constants.ts";
|
|
14
159
|
import { getCommonHeaders } from "./device.ts";
|
|
@@ -258,14 +403,10 @@ function readKimiCliCredentials(): KimiCliCredentialsFile | null {
|
|
|
258
403
|
return null;
|
|
259
404
|
}
|
|
260
405
|
|
|
261
|
-
function kimiCodeCredentialExists(): boolean {
|
|
262
|
-
return existsSync(getKimiCodeCredentialPath());
|
|
263
|
-
}
|
|
264
|
-
|
|
265
406
|
function writeKimiCodeCredentials(access: string, refresh: string, expiresMs: number): void {
|
|
266
407
|
const path = getKimiCodeCredentialPath();
|
|
267
408
|
const dir = dirname(path);
|
|
268
|
-
mkdirSync(dir, { recursive: true });
|
|
409
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
269
410
|
const data: KimiCliCredentialsFile = {
|
|
270
411
|
access_token: access,
|
|
271
412
|
refresh_token: refresh,
|
|
@@ -274,7 +415,24 @@ function writeKimiCodeCredentials(access: string, refresh: string, expiresMs: nu
|
|
|
274
415
|
token_type: "Bearer",
|
|
275
416
|
expires_in: Math.max(0, Math.floor((expiresMs - Date.now()) / 1000)),
|
|
276
417
|
};
|
|
277
|
-
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
|
+
};
|
|
278
436
|
}
|
|
279
437
|
|
|
280
438
|
async function tryReuseKimiCliCredentials(
|
|
@@ -395,26 +553,83 @@ export async function loginKimiCode(callbacks: OAuthLoginCallbacks): Promise<Kim
|
|
|
395
553
|
export async function refreshKimiCodeToken(
|
|
396
554
|
credentials: OAuthCredentials,
|
|
397
555
|
): Promise<KimiOAuthCredentials> {
|
|
398
|
-
const
|
|
399
|
-
let token: Awaited<ReturnType<typeof refreshAccessToken>>;
|
|
556
|
+
const kimiLocked = await lockKimiCodeCredentials();
|
|
400
557
|
try {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
+
}
|
|
406
607
|
throw error;
|
|
407
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(() => {});
|
|
408
632
|
}
|
|
409
|
-
const expiresMs = Date.now() + token.expires_in * 1000;
|
|
410
|
-
writeKimiCodeCredentials(token.access_token, token.refresh_token, expiresMs);
|
|
411
|
-
const extras = await discoverKimiModelMetadata(token.access_token);
|
|
412
|
-
return {
|
|
413
|
-
access: token.access_token,
|
|
414
|
-
refresh: token.refresh_token,
|
|
415
|
-
expires: expiresMs,
|
|
416
|
-
...extras,
|
|
417
|
-
};
|
|
418
633
|
}
|
|
419
634
|
|
|
420
635
|
// =============================================================================
|
|
@@ -424,8 +639,8 @@ export async function refreshKimiCodeToken(
|
|
|
424
639
|
// `expires` is in the past. If the server rotates/revokes the access token
|
|
425
640
|
// before that (common with short-lived session tokens), every request keeps
|
|
426
641
|
// returning 401. The streaming handler invokes refreshKimiAuthToken on the
|
|
427
|
-
// first auth error to force a refresh
|
|
428
|
-
//
|
|
642
|
+
// first auth error to force a refresh and persist the new credentials, and
|
|
643
|
+
// retry once.
|
|
429
644
|
// =============================================================================
|
|
430
645
|
|
|
431
646
|
export function getKimiApiKey(credentials: OAuthCredentials): string {
|
|
@@ -451,75 +666,90 @@ export async function refreshKimiAuthToken(
|
|
|
451
666
|
currentKey: string,
|
|
452
667
|
options: { signal?: AbortSignal } = {},
|
|
453
668
|
): Promise<string | null> {
|
|
669
|
+
let locked: LockedOAuthCredential | undefined;
|
|
670
|
+
let kimiLocked: LockedKimiCodeCredentials | undefined;
|
|
454
671
|
try {
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
const piCred = storage.get(PROVIDER_ID);
|
|
458
|
-
const piOAuth = piCred?.type === "oauth" ? piCred : null;
|
|
672
|
+
locked = await lockStoredOAuthCredential(PROVIDER_ID, options.signal);
|
|
673
|
+
const piOAuth = locked.credential;
|
|
459
674
|
|
|
460
675
|
if (piOAuth) {
|
|
461
676
|
if (piOAuth.access !== currentKey && Date.now() < piOAuth.expires) {
|
|
462
|
-
console.error("[kimi-coding] auth refresh: reusing newer on-disk token");
|
|
463
677
|
return piOAuth.access;
|
|
464
678
|
}
|
|
465
679
|
|
|
466
|
-
console.error("[kimi-coding] auth refresh: requesting new access token");
|
|
467
680
|
let refreshed: Awaited<ReturnType<typeof refreshAccessToken>>;
|
|
681
|
+
kimiLocked = await lockKimiCodeCredentials(options.signal);
|
|
468
682
|
try {
|
|
469
683
|
refreshed = await refreshAccessToken(piOAuth.refresh, { signal: options.signal });
|
|
470
684
|
} catch (error) {
|
|
685
|
+
const kimiCred = readKimiCliCredentials();
|
|
471
686
|
if (!kimiCred?.refresh_token || kimiCred.refresh_token === piOAuth.refresh) throw error;
|
|
472
687
|
const kimiExpiresMs = (kimiCred.expires_at ?? 0) * 1000;
|
|
473
688
|
if (kimiCred.access_token !== currentKey && Date.now() < kimiExpiresMs) {
|
|
474
|
-
const recovered:
|
|
689
|
+
const recovered: StoredOAuthCredential = {
|
|
475
690
|
...piOAuth,
|
|
476
691
|
type: "oauth",
|
|
477
692
|
access: kimiCred.access_token!,
|
|
478
693
|
refresh: kimiCred.refresh_token,
|
|
479
694
|
expires: kimiExpiresMs,
|
|
480
695
|
};
|
|
481
|
-
|
|
482
|
-
console.error("[kimi-coding] auth refresh: recovered newer kimi-code token");
|
|
696
|
+
locked.store(recovered);
|
|
483
697
|
return recovered.access;
|
|
484
698
|
}
|
|
485
|
-
console.error("[kimi-coding] auth refresh: pi token rejected, trying kimi-code token");
|
|
486
699
|
refreshed = await refreshAccessToken(kimiCred.refresh_token, { signal: options.signal });
|
|
487
700
|
}
|
|
488
701
|
|
|
489
702
|
const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
|
|
490
|
-
const
|
|
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);
|
|
720
|
+
const newCred: StoredOAuthCredential = {
|
|
491
721
|
...piOAuth,
|
|
492
722
|
type: "oauth",
|
|
493
723
|
access: refreshed.access_token,
|
|
494
724
|
refresh: refreshed.refresh_token,
|
|
495
725
|
expires: newExpiresMs,
|
|
496
726
|
};
|
|
497
|
-
|
|
498
|
-
writeKimiCodeCredentials(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
|
|
499
|
-
console.error("[kimi-coding] auth refresh: new token persisted");
|
|
727
|
+
locked.store(newCred);
|
|
500
728
|
return newCred.access;
|
|
501
729
|
}
|
|
502
730
|
|
|
731
|
+
kimiLocked = await lockKimiCodeCredentials(options.signal);
|
|
732
|
+
const kimiCred = kimiLocked.credential;
|
|
503
733
|
if (!kimiCred) {
|
|
504
|
-
console.error(
|
|
505
|
-
`[kimi-coding] auth refresh skipped: no OAuth credentials for ${PROVIDER_ID} on disk`,
|
|
506
|
-
);
|
|
507
734
|
return null;
|
|
508
735
|
}
|
|
509
736
|
|
|
510
737
|
const kimiExpiresMs = (kimiCred.expires_at ?? 0) * 1000;
|
|
511
738
|
if (kimiCred.access_token !== currentKey && Date.now() < kimiExpiresMs) {
|
|
512
|
-
console.error("[kimi-coding] auth refresh: reusing newer kimi-code token");
|
|
513
739
|
return kimiCred.access_token!;
|
|
514
740
|
}
|
|
515
741
|
|
|
516
|
-
console.error("[kimi-coding] auth refresh: requesting new access token via kimi-code");
|
|
517
742
|
const refreshed = await refreshAccessToken(kimiCred.refresh_token!, {
|
|
518
743
|
signal: options.signal,
|
|
519
744
|
});
|
|
520
745
|
const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
|
|
521
|
-
|
|
522
|
-
|
|
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);
|
|
523
753
|
return refreshed.access_token;
|
|
524
754
|
} catch (err) {
|
|
525
755
|
if (options.signal?.aborted) return null;
|
|
@@ -529,5 +759,12 @@ export async function refreshKimiAuthToken(
|
|
|
529
759
|
console.error("[kimi-coding] auth refresh failed:", err);
|
|
530
760
|
}
|
|
531
761
|
return null;
|
|
762
|
+
} finally {
|
|
763
|
+
if (kimiLocked) {
|
|
764
|
+
await kimiLocked.release().catch(() => {});
|
|
765
|
+
}
|
|
766
|
+
if (locked) {
|
|
767
|
+
await locked.release().catch(() => {});
|
|
768
|
+
}
|
|
532
769
|
}
|
|
533
770
|
}
|
package/src/stream.ts
CHANGED
|
@@ -12,11 +12,46 @@ import type {
|
|
|
12
12
|
Model,
|
|
13
13
|
SimpleStreamOptions,
|
|
14
14
|
} from "@earendil-works/pi-ai";
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
import * as piAi from "@earendil-works/pi-ai";
|
|
16
|
+
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
17
|
+
|
|
18
|
+
type KimiStreamSimple = (
|
|
19
|
+
model: Model<Api>,
|
|
20
|
+
context: Context,
|
|
21
|
+
options?: SimpleStreamOptions,
|
|
22
|
+
) => AssistantMessageEventStream;
|
|
23
|
+
|
|
24
|
+
// pi 0.80.8 unexported the global streamSimple* functions. Depending on how
|
|
25
|
+
// this module is loaded, the pi-ai root now offers one of three shapes:
|
|
26
|
+
// - pi extension loader (jiti/Bun): root aliases to the compat entrypoint,
|
|
27
|
+
// which exposes lazy per-API factories (anthropicMessagesApi et al.)
|
|
28
|
+
// - pi <=0.79: root only has the legacy streamSimple* globals
|
|
29
|
+
// - plain Node with pi-ai >=0.80.8 (tests, SDK use): root has neither, but
|
|
30
|
+
// lazyApi + a dynamic subpath import reaches the same implementation
|
|
31
|
+
// Detect whichever shape is present so the extension loads in all three.
|
|
32
|
+
const piAiRuntime = piAi as unknown as {
|
|
33
|
+
anthropicMessagesApi?: () => { streamSimple: KimiStreamSimple };
|
|
34
|
+
openAICompletionsApi?: () => { streamSimple: KimiStreamSimple };
|
|
35
|
+
streamSimpleAnthropic?: KimiStreamSimple;
|
|
36
|
+
streamSimpleOpenAICompletions?: KimiStreamSimple;
|
|
37
|
+
lazyApi?: (load: () => Promise<object>) => { streamSimple: KimiStreamSimple };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// The ./api/* subpath exports only exist on pi-ai >=0.80.8, so a literal
|
|
41
|
+
// specifier would fail type resolution against older pi-ai. Computed
|
|
42
|
+
// specifiers keep the imports invisible to TypeScript; they are only ever
|
|
43
|
+
// evaluated in plain-Node environments where the subpaths do resolve.
|
|
44
|
+
const anthropicMessagesModule = "@earendil-works/pi-ai/api/anthropic-messages";
|
|
45
|
+
const openAICompletionsModule = "@earendil-works/pi-ai/api/openai-completions";
|
|
46
|
+
|
|
47
|
+
const streamSimpleAnthropic: KimiStreamSimple =
|
|
48
|
+
piAiRuntime.anthropicMessagesApi?.().streamSimple ??
|
|
49
|
+
piAiRuntime.streamSimpleAnthropic ??
|
|
50
|
+
piAiRuntime.lazyApi?.(() => import(anthropicMessagesModule)).streamSimple!;
|
|
51
|
+
const streamSimpleOpenAICompletions: KimiStreamSimple =
|
|
52
|
+
piAiRuntime.openAICompletionsApi?.().streamSimple ??
|
|
53
|
+
piAiRuntime.streamSimpleOpenAICompletions ??
|
|
54
|
+
piAiRuntime.lazyApi?.(() => import(openAICompletionsModule)).streamSimple!;
|
|
20
55
|
import {
|
|
21
56
|
DEFAULT_KIMI_CODE_CONFIG,
|
|
22
57
|
type KimiCodeConfig,
|
|
@@ -162,7 +197,16 @@ export function resolveKimiApiKey(apiKey: string | undefined): string {
|
|
|
162
197
|
return apiKey || process.env.KIMI_API_KEY || "";
|
|
163
198
|
}
|
|
164
199
|
|
|
165
|
-
|
|
200
|
+
// SimpleStreamOptions.headers is Record<string, string> on pi <=0.79 and
|
|
201
|
+
// ProviderHeaders (Record<string, string | null>, null = suppress the header
|
|
202
|
+
// when pi-ai assembles the request) on >=0.80.8. Accept and preserve both
|
|
203
|
+
// shapes; defined locally because ProviderHeaders is not exported on old pi.
|
|
204
|
+
type HeaderMap = Record<string, string | null>;
|
|
205
|
+
|
|
206
|
+
export function mergeKimiRequestHeaders(headers?: HeaderMap): HeaderMap {
|
|
207
|
+
// Caller values override our Kimi defaults. Nulls are retained rather than
|
|
208
|
+
// deleted so the suppression semantics reach pi-ai, which strips null
|
|
209
|
+
// entries (including its own defaults) when building the request.
|
|
166
210
|
return { ...getKimiProviderHeaders(), ...headers };
|
|
167
211
|
}
|
|
168
212
|
|
|
@@ -231,7 +275,9 @@ export function streamSimpleKimi(
|
|
|
231
275
|
return {
|
|
232
276
|
...options,
|
|
233
277
|
...apiKeyOverride,
|
|
234
|
-
headers:
|
|
278
|
+
// Cast to whatever headers shape the installed pi-ai declares:
|
|
279
|
+
// Record<string, string> on pi <=0.79, ProviderHeaders on >=0.80.8.
|
|
280
|
+
headers: mergeKimiRequestHeaders(options?.headers) as SimpleStreamOptions["headers"],
|
|
235
281
|
onPayload: async (payload, modelData) => {
|
|
236
282
|
let nextPayload: unknown = payload;
|
|
237
283
|
|
package/src/tools/common.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
|
|
4
4
|
import { PROVIDER_ID, getBaseUrl } from "../constants.ts";
|
|
5
5
|
import { getKimiProviderHeaders } from "../device.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
KIMI_LOGIN_REQUIRED_MESSAGE,
|
|
8
|
+
readStoredOAuthCredential,
|
|
9
|
+
refreshKimiAuthToken,
|
|
10
|
+
} from "../oauth.ts";
|
|
7
11
|
|
|
8
12
|
export const KIMI_TOOL_TIMEOUT_MS = 180_000;
|
|
9
13
|
|
|
@@ -19,9 +23,7 @@ export interface BuildKimiToolOptions {
|
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
function defaultGetAccessToken(): string | null {
|
|
22
|
-
|
|
23
|
-
if (!credential || credential.type !== "oauth") return null;
|
|
24
|
-
return credential.access;
|
|
26
|
+
return readStoredOAuthCredential(PROVIDER_ID)?.access ?? null;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export function buildKimiToolDeps(options: BuildKimiToolOptions = {}): KimiToolDeps {
|
package/src/usage.ts
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
|
-
import { AuthStorage } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
1
|
import { PROVIDER_ID, getBaseUrl } from "./constants.ts";
|
|
4
2
|
import { getKimiProviderHeaders } from "./device.ts";
|
|
5
|
-
import { refreshKimiAuthToken } from "./oauth.ts";
|
|
6
|
-
|
|
7
|
-
const MEMBERSHIP_LEVEL_NAMES: Record<string, string> = {
|
|
8
|
-
LEVEL_FREE: "Free",
|
|
9
|
-
LEVEL_BASIC: "Adagio",
|
|
10
|
-
LEVEL_STANDARD: "Moderato",
|
|
11
|
-
LEVEL_INTERMEDIATE: "Allegretto",
|
|
12
|
-
LEVEL_ADVANCED: "Allegro",
|
|
13
|
-
LEVEL_PREMIUM: "Vivace",
|
|
14
|
-
};
|
|
3
|
+
import { readStoredOAuthCredential, refreshKimiAuthToken } from "./oauth.ts";
|
|
15
4
|
|
|
16
5
|
const RESET_TIME_KEYS = [
|
|
17
6
|
"resetTime",
|
|
@@ -38,7 +27,6 @@ export interface UsageFormatOptions {
|
|
|
38
27
|
|
|
39
28
|
export interface KimiUsageSnapshot {
|
|
40
29
|
summary: string;
|
|
41
|
-
membershipLevel: string | null;
|
|
42
30
|
}
|
|
43
31
|
|
|
44
32
|
interface BoosterWalletInfo {
|
|
@@ -52,8 +40,8 @@ interface BoosterWalletInfo {
|
|
|
52
40
|
const FIXED_POINT_CENTS = 1_000_000n;
|
|
53
41
|
|
|
54
42
|
export function getKimiUsageToken(): string | null {
|
|
55
|
-
const credential =
|
|
56
|
-
if (credential?.
|
|
43
|
+
const credential = readStoredOAuthCredential(PROVIDER_ID);
|
|
44
|
+
if (credential?.access) return credential.access;
|
|
57
45
|
const apiKey = process.env.KIMI_API_KEY?.trim();
|
|
58
46
|
return apiKey || null;
|
|
59
47
|
}
|
|
@@ -176,9 +164,6 @@ export function parseUsageSummary(payload: unknown, options: UsageFormatOptions
|
|
|
176
164
|
|
|
177
165
|
const record = payload as Record<string, unknown>;
|
|
178
166
|
const lines: string[] = [];
|
|
179
|
-
const membership = parseMembership(record);
|
|
180
|
-
if (membership) lines.push(membership, "");
|
|
181
|
-
|
|
182
167
|
const summary = parseUsageRow(record.usage, "Current week");
|
|
183
168
|
if (summary)
|
|
184
169
|
lines.push(formatUsageRow({ ...summary, label: normalizeUsageLabel(summary.label) }, options));
|
|
@@ -237,24 +222,6 @@ function formatWindowLabel(value: unknown, fallbackLabel: string): string {
|
|
|
237
222
|
return `Current ${minutes}m window`;
|
|
238
223
|
}
|
|
239
224
|
|
|
240
|
-
export function parseMembershipLevel(record: Record<string, unknown>): string | null {
|
|
241
|
-
const user = record.user;
|
|
242
|
-
if (typeof user !== "object" || user === null || Array.isArray(user)) return null;
|
|
243
|
-
const membership = (user as Record<string, unknown>).membership;
|
|
244
|
-
if (typeof membership !== "object" || membership === null || Array.isArray(membership)) {
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
247
|
-
const level = (membership as Record<string, unknown>).level;
|
|
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;
|
|
254
|
-
const name = MEMBERSHIP_LEVEL_NAMES[level];
|
|
255
|
-
return name ? `Membership: ${name} (${level})` : `Membership: ${level}`;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
225
|
export function formatUsageRow(row: UsageRow, options: UsageFormatOptions = {}): string {
|
|
259
226
|
if (row.limit <= 0) return `${row.label}\n${row.used} used`;
|
|
260
227
|
const used = Math.max(0, Math.min(row.used, row.limit));
|
|
@@ -358,10 +325,7 @@ export async function fetchKimiUsageSnapshot(
|
|
|
358
325
|
): Promise<KimiUsageSnapshot> {
|
|
359
326
|
const token = options.token ?? getKimiUsageToken();
|
|
360
327
|
if (!token) {
|
|
361
|
-
return {
|
|
362
|
-
summary: "Usage: missing credentials. Run /login kimi-coding.",
|
|
363
|
-
membershipLevel: null,
|
|
364
|
-
};
|
|
328
|
+
return { summary: "Usage: missing credentials. Run /login kimi-coding." };
|
|
365
329
|
}
|
|
366
330
|
|
|
367
331
|
const controller = new AbortController();
|
|
@@ -373,17 +337,13 @@ export async function fetchKimiUsageSnapshot(
|
|
|
373
337
|
if (refreshed) response = await fetchKimiUsage(refreshed, controller.signal);
|
|
374
338
|
}
|
|
375
339
|
if (!response.ok) {
|
|
376
|
-
return { summary: `Usage: fetch failed (${response.status})
|
|
340
|
+
return { summary: `Usage: fetch failed (${response.status})` };
|
|
377
341
|
}
|
|
378
342
|
const payload = (await response.json()) as unknown;
|
|
379
|
-
|
|
380
|
-
typeof payload === "object" && payload !== null && !Array.isArray(payload)
|
|
381
|
-
? parseMembershipLevel(payload as Record<string, unknown>)
|
|
382
|
-
: null;
|
|
383
|
-
return { summary: parseUsageSummary(payload), membershipLevel };
|
|
343
|
+
return { summary: parseUsageSummary(payload) };
|
|
384
344
|
} catch (error) {
|
|
385
345
|
const message = error instanceof Error ? error.message : String(error);
|
|
386
|
-
return { summary: `Usage: fetch failed (${message})
|
|
346
|
+
return { summary: `Usage: fetch failed (${message})` };
|
|
387
347
|
} finally {
|
|
388
348
|
clearTimeout(timeout);
|
|
389
349
|
}
|