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 +11 -11
- package/index.ts +62 -69
- package/package.json +1 -1
- package/src/constants.ts +2 -2
- package/src/models.ts +87 -57
- package/src/oauth.ts +225 -67
- package/src/usage.ts +4 -42
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
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
|
@@ -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
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
90
|
+
minTimeout: 100,
|
|
91
|
+
maxTimeout: 10_000,
|
|
60
92
|
});
|
|
61
93
|
}
|
|
62
94
|
|
|
63
|
-
async function
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
data =
|
|
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
|
-
|
|
87
|
-
|
|
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
|
|
480
|
-
let token: Awaited<ReturnType<typeof refreshAccessToken>>;
|
|
556
|
+
const kimiLocked = await lockKimiCodeCredentials();
|
|
481
557
|
try {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
537
|
-
const piOAuth =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
601
|
-
|
|
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})
|
|
340
|
+
return { summary: `Usage: fetch failed (${response.status})` };
|
|
375
341
|
}
|
|
376
342
|
const payload = (await response.json()) as unknown;
|
|
377
|
-
|
|
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})
|
|
346
|
+
return { summary: `Usage: fetch failed (${message})` };
|
|
385
347
|
} finally {
|
|
386
348
|
clearTimeout(timeout);
|
|
387
349
|
}
|