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