pi-provider-kimi-code 0.6.5 → 0.6.7
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 +8 -4
- package/src/config.ts +3 -2
- package/src/constants.ts +3 -3
- package/src/models.ts +61 -5
- package/src/oauth.ts +125 -19
- package/src/payload.ts +22 -0
- package/src/stream.ts +53 -7
- package/src/tools/common.ts +7 -5
- package/src/usage.ts +42 -16
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-provider-kimi-code",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
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/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.7";
|
|
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
|
@@ -2,13 +2,94 @@
|
|
|
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 acquireAuthFileLock(authPath: string): Promise<() => Promise<void>> {
|
|
56
|
+
return acquireFileLock(authPath, {
|
|
57
|
+
realpath: false,
|
|
58
|
+
stale: 30_000,
|
|
59
|
+
retries: { retries: 10, factor: 2, minTimeout: 100, maxTimeout: 10_000, randomize: true },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function writeStoredCredential(
|
|
64
|
+
providerId: string,
|
|
65
|
+
credential: StoredOAuthCredential,
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
const AuthStorage = piAgentRuntime.AuthStorage;
|
|
68
|
+
if (AuthStorage) {
|
|
69
|
+
AuthStorage.create().set(providerId, credential);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// pi >=0.80.8: no exported write path, so do a locked read-modify-write of
|
|
73
|
+
// auth.json in pi's on-disk format (indent 2). The read happens inside the
|
|
74
|
+
// lock so concurrent updates from other processes are never clobbered, and
|
|
75
|
+
// the explicit chmod covers pre-existing files (writeFileSync's mode only
|
|
76
|
+
// applies on creation).
|
|
77
|
+
const authPath = join(piAgentRuntime.getAgentDir(), "auth.json");
|
|
78
|
+
const release = await acquireAuthFileLock(authPath);
|
|
79
|
+
try {
|
|
80
|
+
let data: Record<string, unknown> = {};
|
|
81
|
+
try {
|
|
82
|
+
data = JSON.parse(readFileSync(authPath, "utf-8"));
|
|
83
|
+
} catch {
|
|
84
|
+
// Missing or unreadable auth.json — start from an empty store.
|
|
85
|
+
}
|
|
86
|
+
data[providerId] = credential;
|
|
87
|
+
writeFileSync(authPath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 0o600 });
|
|
88
|
+
chmodSync(authPath, 0o600);
|
|
89
|
+
} finally {
|
|
90
|
+
await release();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
12
93
|
|
|
13
94
|
import { CLIENT_ID, PROVIDER_ID, RETRYABLE_REFRESH_STATUSES, getOAuthHost } from "./constants.ts";
|
|
14
95
|
import { getCommonHeaders } from "./device.ts";
|
|
@@ -120,6 +201,7 @@ export async function requestDeviceToken(
|
|
|
120
201
|
|
|
121
202
|
interface RefreshAccessTokenOptions {
|
|
122
203
|
maxRetries?: number;
|
|
204
|
+
signal?: AbortSignal;
|
|
123
205
|
sleep?: (ms: number) => Promise<void>;
|
|
124
206
|
}
|
|
125
207
|
|
|
@@ -133,8 +215,20 @@ export class KimiLoginRequiredError extends Error {
|
|
|
133
215
|
}
|
|
134
216
|
}
|
|
135
217
|
|
|
136
|
-
function sleep(ms: number): Promise<void> {
|
|
137
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
218
|
+
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
219
|
+
if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
|
|
220
|
+
signal.throwIfAborted();
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
const onAbort = () => {
|
|
223
|
+
clearTimeout(timeout);
|
|
224
|
+
reject(signal.reason);
|
|
225
|
+
};
|
|
226
|
+
const timeout = setTimeout(() => {
|
|
227
|
+
signal.removeEventListener("abort", onAbort);
|
|
228
|
+
resolve();
|
|
229
|
+
}, ms);
|
|
230
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
231
|
+
});
|
|
138
232
|
}
|
|
139
233
|
|
|
140
234
|
export async function refreshAccessToken(
|
|
@@ -142,11 +236,12 @@ export async function refreshAccessToken(
|
|
|
142
236
|
options: RefreshAccessTokenOptions = {},
|
|
143
237
|
): Promise<TokenResponse> {
|
|
144
238
|
const maxRetries = options.maxRetries ?? 3;
|
|
145
|
-
const wait = options.sleep ?? sleep;
|
|
239
|
+
const wait = options.sleep ?? ((ms: number) => sleep(ms, options.signal));
|
|
146
240
|
let lastError: unknown;
|
|
147
241
|
|
|
148
242
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
149
243
|
try {
|
|
244
|
+
options.signal?.throwIfAborted();
|
|
150
245
|
const response = await fetch(`${getOAuthHost()}/api/oauth/token`, {
|
|
151
246
|
method: "POST",
|
|
152
247
|
headers: {
|
|
@@ -158,6 +253,7 @@ export async function refreshAccessToken(
|
|
|
158
253
|
grant_type: "refresh_token",
|
|
159
254
|
refresh_token: refreshToken,
|
|
160
255
|
}),
|
|
256
|
+
signal: options.signal,
|
|
161
257
|
});
|
|
162
258
|
|
|
163
259
|
if (!response.ok) {
|
|
@@ -185,6 +281,7 @@ export async function refreshAccessToken(
|
|
|
185
281
|
|
|
186
282
|
return data;
|
|
187
283
|
} catch (error) {
|
|
284
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
188
285
|
lastError = error;
|
|
189
286
|
const message = error instanceof Error ? error.message : String(error);
|
|
190
287
|
if (error instanceof KimiLoginRequiredError) throw error;
|
|
@@ -408,8 +505,8 @@ export async function refreshKimiCodeToken(
|
|
|
408
505
|
// `expires` is in the past. If the server rotates/revokes the access token
|
|
409
506
|
// before that (common with short-lived session tokens), every request keeps
|
|
410
507
|
// returning 401. The streaming handler invokes refreshKimiAuthToken on the
|
|
411
|
-
// first auth error to force a refresh
|
|
412
|
-
//
|
|
508
|
+
// first auth error to force a refresh and persist the new credentials, and
|
|
509
|
+
// retry once.
|
|
413
510
|
// =============================================================================
|
|
414
511
|
|
|
415
512
|
export function getKimiApiKey(credentials: OAuthCredentials): string {
|
|
@@ -418,6 +515,11 @@ export function getKimiApiKey(credentials: OAuthCredentials): string {
|
|
|
418
515
|
|
|
419
516
|
export function isKimiAuthErrorMessage(message: unknown): boolean {
|
|
420
517
|
const text = String(message ?? "").toLowerCase();
|
|
518
|
+
const isMembershipPermissionError =
|
|
519
|
+
text.includes("current subscription does not have access to k3") ||
|
|
520
|
+
text.includes("current plan supports only kimi-k3 up to 256k context") ||
|
|
521
|
+
text.includes("current subscription does not have access to kimi-for-coding-highspeed");
|
|
522
|
+
if (isMembershipPermissionError) return false;
|
|
421
523
|
return (
|
|
422
524
|
/\b401\b/.test(text) ||
|
|
423
525
|
text.includes("unauthorized") ||
|
|
@@ -426,12 +528,13 @@ export function isKimiAuthErrorMessage(message: unknown): boolean {
|
|
|
426
528
|
);
|
|
427
529
|
}
|
|
428
530
|
|
|
429
|
-
export async function refreshKimiAuthToken(
|
|
531
|
+
export async function refreshKimiAuthToken(
|
|
532
|
+
currentKey: string,
|
|
533
|
+
options: { signal?: AbortSignal } = {},
|
|
534
|
+
): Promise<string | null> {
|
|
430
535
|
try {
|
|
431
536
|
const kimiCred = readKimiCliCredentials();
|
|
432
|
-
const
|
|
433
|
-
const piCred = storage.get(PROVIDER_ID);
|
|
434
|
-
const piOAuth = piCred?.type === "oauth" ? piCred : null;
|
|
537
|
+
const piOAuth = readStoredOAuthCredential(PROVIDER_ID);
|
|
435
538
|
|
|
436
539
|
if (piOAuth) {
|
|
437
540
|
if (piOAuth.access !== currentKey && Date.now() < piOAuth.expires) {
|
|
@@ -442,35 +545,35 @@ export async function refreshKimiAuthToken(currentKey: string): Promise<string |
|
|
|
442
545
|
console.error("[kimi-coding] auth refresh: requesting new access token");
|
|
443
546
|
let refreshed: Awaited<ReturnType<typeof refreshAccessToken>>;
|
|
444
547
|
try {
|
|
445
|
-
refreshed = await refreshAccessToken(piOAuth.refresh);
|
|
548
|
+
refreshed = await refreshAccessToken(piOAuth.refresh, { signal: options.signal });
|
|
446
549
|
} catch (error) {
|
|
447
550
|
if (!kimiCred?.refresh_token || kimiCred.refresh_token === piOAuth.refresh) throw error;
|
|
448
551
|
const kimiExpiresMs = (kimiCred.expires_at ?? 0) * 1000;
|
|
449
552
|
if (kimiCred.access_token !== currentKey && Date.now() < kimiExpiresMs) {
|
|
450
|
-
const recovered:
|
|
553
|
+
const recovered: StoredOAuthCredential = {
|
|
451
554
|
...piOAuth,
|
|
452
555
|
type: "oauth",
|
|
453
556
|
access: kimiCred.access_token!,
|
|
454
557
|
refresh: kimiCred.refresh_token,
|
|
455
558
|
expires: kimiExpiresMs,
|
|
456
559
|
};
|
|
457
|
-
|
|
560
|
+
await writeStoredCredential(PROVIDER_ID, recovered);
|
|
458
561
|
console.error("[kimi-coding] auth refresh: recovered newer kimi-code token");
|
|
459
562
|
return recovered.access;
|
|
460
563
|
}
|
|
461
564
|
console.error("[kimi-coding] auth refresh: pi token rejected, trying kimi-code token");
|
|
462
|
-
refreshed = await refreshAccessToken(kimiCred.refresh_token);
|
|
565
|
+
refreshed = await refreshAccessToken(kimiCred.refresh_token, { signal: options.signal });
|
|
463
566
|
}
|
|
464
567
|
|
|
465
568
|
const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
|
|
466
|
-
const newCred:
|
|
569
|
+
const newCred: StoredOAuthCredential = {
|
|
467
570
|
...piOAuth,
|
|
468
571
|
type: "oauth",
|
|
469
572
|
access: refreshed.access_token,
|
|
470
573
|
refresh: refreshed.refresh_token,
|
|
471
574
|
expires: newExpiresMs,
|
|
472
575
|
};
|
|
473
|
-
|
|
576
|
+
await writeStoredCredential(PROVIDER_ID, newCred);
|
|
474
577
|
writeKimiCodeCredentials(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
|
|
475
578
|
console.error("[kimi-coding] auth refresh: new token persisted");
|
|
476
579
|
return newCred.access;
|
|
@@ -490,12 +593,15 @@ export async function refreshKimiAuthToken(currentKey: string): Promise<string |
|
|
|
490
593
|
}
|
|
491
594
|
|
|
492
595
|
console.error("[kimi-coding] auth refresh: requesting new access token via kimi-code");
|
|
493
|
-
const refreshed = await refreshAccessToken(kimiCred.refresh_token
|
|
596
|
+
const refreshed = await refreshAccessToken(kimiCred.refresh_token!, {
|
|
597
|
+
signal: options.signal,
|
|
598
|
+
});
|
|
494
599
|
const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
|
|
495
600
|
writeKimiCodeCredentials(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
|
|
496
601
|
console.error("[kimi-coding] auth refresh: new token persisted to kimi-code");
|
|
497
602
|
return refreshed.access_token;
|
|
498
603
|
} catch (err) {
|
|
604
|
+
if (options.signal?.aborted) return null;
|
|
499
605
|
if (err instanceof KimiLoginRequiredError) {
|
|
500
606
|
console.error(`[kimi-coding] ${KIMI_LOGIN_REQUIRED_MESSAGE}`);
|
|
501
607
|
} 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/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,8 +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";
|
|
3
|
+
import { readStoredOAuthCredential, refreshKimiAuthToken } from "./oauth.ts";
|
|
6
4
|
|
|
7
5
|
const MEMBERSHIP_LEVEL_NAMES: Record<string, string> = {
|
|
8
6
|
LEVEL_FREE: "Free",
|
|
@@ -36,6 +34,11 @@ export interface UsageFormatOptions {
|
|
|
36
34
|
timeZone?: string;
|
|
37
35
|
}
|
|
38
36
|
|
|
37
|
+
export interface KimiUsageSnapshot {
|
|
38
|
+
summary: string;
|
|
39
|
+
membershipLevel: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
39
42
|
interface BoosterWalletInfo {
|
|
40
43
|
balanceCents: bigint;
|
|
41
44
|
monthlyChargeLimitEnabled: boolean;
|
|
@@ -47,8 +50,8 @@ interface BoosterWalletInfo {
|
|
|
47
50
|
const FIXED_POINT_CENTS = 1_000_000n;
|
|
48
51
|
|
|
49
52
|
export function getKimiUsageToken(): string | null {
|
|
50
|
-
const credential =
|
|
51
|
-
if (credential?.
|
|
53
|
+
const credential = readStoredOAuthCredential(PROVIDER_ID);
|
|
54
|
+
if (credential?.access) return credential.access;
|
|
52
55
|
const apiKey = process.env.KIMI_API_KEY?.trim();
|
|
53
56
|
return apiKey || null;
|
|
54
57
|
}
|
|
@@ -232,7 +235,7 @@ function formatWindowLabel(value: unknown, fallbackLabel: string): string {
|
|
|
232
235
|
return `Current ${minutes}m window`;
|
|
233
236
|
}
|
|
234
237
|
|
|
235
|
-
export function
|
|
238
|
+
export function parseMembershipLevel(record: Record<string, unknown>): string | null {
|
|
236
239
|
const user = record.user;
|
|
237
240
|
if (typeof user !== "object" || user === null || Array.isArray(user)) return null;
|
|
238
241
|
const membership = (user as Record<string, unknown>).membership;
|
|
@@ -240,7 +243,12 @@ export function parseMembership(record: Record<string, unknown>): string | null
|
|
|
240
243
|
return null;
|
|
241
244
|
}
|
|
242
245
|
const level = (membership as Record<string, unknown>).level;
|
|
243
|
-
|
|
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;
|
|
244
252
|
const name = MEMBERSHIP_LEVEL_NAMES[level];
|
|
245
253
|
return name ? `Membership: ${name} (${level})` : `Membership: ${level}`;
|
|
246
254
|
}
|
|
@@ -343,24 +351,42 @@ function fetchKimiUsage(token: string, signal: AbortSignal): Promise<Response> {
|
|
|
343
351
|
});
|
|
344
352
|
}
|
|
345
353
|
|
|
346
|
-
export async function
|
|
347
|
-
|
|
348
|
-
|
|
354
|
+
export async function fetchKimiUsageSnapshot(
|
|
355
|
+
options: { timeoutMs?: number; token?: string; refreshOnUnauthorized?: boolean } = {},
|
|
356
|
+
): Promise<KimiUsageSnapshot> {
|
|
357
|
+
const token = options.token ?? getKimiUsageToken();
|
|
358
|
+
if (!token) {
|
|
359
|
+
return {
|
|
360
|
+
summary: "Usage: missing credentials. Run /login kimi-coding.",
|
|
361
|
+
membershipLevel: null,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
349
364
|
|
|
350
365
|
const controller = new AbortController();
|
|
351
|
-
const timeout = setTimeout(() => controller.abort(), 15_000);
|
|
366
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 15_000);
|
|
352
367
|
try {
|
|
353
368
|
let response = await fetchKimiUsage(token, controller.signal);
|
|
354
|
-
if (response.status === 401) {
|
|
355
|
-
const refreshed = await refreshKimiAuthToken(token);
|
|
369
|
+
if (response.status === 401 && options.refreshOnUnauthorized !== false) {
|
|
370
|
+
const refreshed = await refreshKimiAuthToken(token, { signal: controller.signal });
|
|
356
371
|
if (refreshed) response = await fetchKimiUsage(refreshed, controller.signal);
|
|
357
372
|
}
|
|
358
|
-
if (!response.ok)
|
|
359
|
-
|
|
373
|
+
if (!response.ok) {
|
|
374
|
+
return { summary: `Usage: fetch failed (${response.status})`, membershipLevel: null };
|
|
375
|
+
}
|
|
376
|
+
const payload = (await response.json()) as unknown;
|
|
377
|
+
const membershipLevel =
|
|
378
|
+
typeof payload === "object" && payload !== null && !Array.isArray(payload)
|
|
379
|
+
? parseMembershipLevel(payload as Record<string, unknown>)
|
|
380
|
+
: null;
|
|
381
|
+
return { summary: parseUsageSummary(payload), membershipLevel };
|
|
360
382
|
} catch (error) {
|
|
361
383
|
const message = error instanceof Error ? error.message : String(error);
|
|
362
|
-
return `Usage: fetch failed (${message})
|
|
384
|
+
return { summary: `Usage: fetch failed (${message})`, membershipLevel: null };
|
|
363
385
|
} finally {
|
|
364
386
|
clearTimeout(timeout);
|
|
365
387
|
}
|
|
366
388
|
}
|
|
389
|
+
|
|
390
|
+
export async function fetchKimiUsageSummary(): Promise<string> {
|
|
391
|
+
return (await fetchKimiUsageSnapshot()).summary;
|
|
392
|
+
}
|