pi-provider-kimi-code 0.6.8 → 0.6.10
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 +48 -112
- package/index.ts +195 -25
- package/package.json +9 -5
- package/src/constants.ts +6 -2
- package/src/device.ts +59 -17
- package/src/models.ts +61 -6
- package/src/oauth.ts +7 -1
- package/src/payload.ts +154 -40
- package/src/stream.ts +32 -4
- package/src/tools/datasource.ts +22 -1
- package/src/usage.ts +98 -8
package/README.md
CHANGED
|
@@ -3,40 +3,29 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/pi-provider-kimi-code)
|
|
4
4
|
[](./LICENSE)
|
|
5
5
|
|
|
6
|
-
**
|
|
7
|
-
|
|
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
|
-
|
|
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.
|
|
6
|
+
> **Kimi Code extension for Pi — use K3, K2.7 Code, and HighSpeed models with your Kimi Code Plan.** This extension brings the full Kimi Code surface into Pi: shared OAuth login state with the official `kimi-code` CLI, Kimi Files API uploads, and Kimi-native tools such as `moonshot_search`, `moonshot_fetch`, and `kimi_datasource`. (Pi v0.82.0+ ships a built-in `kimi-coding` provider for the LLM layer; this extension covers everything around it.)
|
|
11
7
|
|
|
12
8
|
## Why this exists
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Kimi's API surface goes beyond the LLM itself. It includes file uploads, web search, page fetch, and a growing set of datasource APIs. This extension wires those capabilities into Pi so you can use them alongside the community's existing extensions — MCP servers, custom themes, skills, and any other Pi add-on — without leaving the harness.
|
|
10
|
+
Use the built-in provider if you only want to chat with Kimi models. Install this extension when you also want the full Kimi Code surface inside Pi:
|
|
17
11
|
|
|
18
|
-
- **
|
|
12
|
+
- **Share a session with the official `kimi-code` CLI.** Reuse an existing `kimi-code` login, or sync Pi's OAuth login back to `kimi-code` so both CLIs stay authenticated.
|
|
19
13
|
- **Send files the Kimi way.** Large inline images go through Kimi's Files API and become `ms://` references instead of huge base64 payloads.
|
|
20
|
-
- **
|
|
14
|
+
- **Use Kimi-native tools.** `moonshot_search`, `moonshot_fetch`, and `kimi_datasource` are opt-in tools that call Moonshot's server-side services.
|
|
21
15
|
- **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
|
-
- **
|
|
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
|
-
- **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.
|
|
16
|
+
- **Embed in your own build.** The `KimiCode()` factory lets you ship Kimi Code support inside a custom Pi agent with programmatic config overrides — no file-based extension path needed.
|
|
25
17
|
|
|
26
18
|
## What this package adds
|
|
27
19
|
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
- Optional `moonshot_search`, `moonshot_fetch`, and unified datasource `kimi_datasource` tools via `/kimi-settings`.
|
|
38
|
-
- Programmatic `KimiCode()` factory for embedding in custom Pi builds.
|
|
39
|
-
- No build step; Pi loads the TypeScript extension directly.
|
|
20
|
+
- **Credential reuse with `kimi-code`.** Reads `~/.kimi-code/credentials/kimi-code.json` and syncs refreshed OAuth tokens back to it.
|
|
21
|
+
- **Kimi account login in Pi.** `/login kimi-coding` stores credentials in `~/.pi/agent/auth.json`; tokens refresh automatically.
|
|
22
|
+
- **`KIMI_API_KEY` fallback.** For CI or key-based access.
|
|
23
|
+
- **Kimi Files API uploads.** Uploads large inline images and references them as `ms://` instead of inline base64.
|
|
24
|
+
- **Optional Kimi-native tools.** `moonshot_search`, `moonshot_fetch`, and `kimi_datasource` via `/kimi-settings`.
|
|
25
|
+
- **Tool schema dedup.** Collapses repeated `$ref`/`$defs` structures to stay under Moonshot's 15 KB per-tool limit.
|
|
26
|
+
- **Legacy provider registration.** Keeps existing model sessions loading; new users should use Pi's built-in provider.
|
|
27
|
+
- **Programmatic `KimiCode()` factory.** Embed Kimi Code in a custom Pi build with config overrides.
|
|
28
|
+
- **No build step.** Pi loads the TypeScript extension directly.
|
|
40
29
|
|
|
41
30
|
## Install
|
|
42
31
|
|
|
@@ -76,57 +65,32 @@ main(process.argv.slice(2), {
|
|
|
76
65
|
|
|
77
66
|
`KimiCode()` with no arguments behaves identically to the file-based extension. Pass a `KimiCodeConfigPatch` to override defaults (protocol, upload threshold, tools, model parameters). See [docs/programmatic-usage.md](docs/programmatic-usage.md) for the full API.
|
|
78
67
|
|
|
79
|
-
##
|
|
80
|
-
|
|
81
|
-
Inside Pi, run:
|
|
82
|
-
|
|
83
|
-
```text
|
|
84
|
-
/login kimi-coding
|
|
85
|
-
```
|
|
68
|
+
## Authentication
|
|
86
69
|
|
|
87
|
-
|
|
70
|
+
Pi's built-in `kimi-coding` provider already supports OAuth login via `/login`. This extension handles the credential layer around that login:
|
|
88
71
|
|
|
89
|
-
|
|
90
|
-
|
|
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:
|
|
96
|
-
|
|
97
|
-
```text
|
|
98
|
-
~/.kimi/credentials/kimi-code.json
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
Legacy credentials are read-only. Set `KIMI_SHARE_DIR` to override the legacy `~/.kimi` directory.
|
|
102
|
-
|
|
103
|
-
For CI or pay-per-token use, set `KIMI_API_KEY` instead:
|
|
72
|
+
- **Syncs refreshed tokens back to `kimi-code`.** Pi stores credentials at `~/.pi/agent/auth.json`; this extension keeps the official CLI's credential file (`~/.kimi-code/credentials/kimi-code.json`) up to date so both stay authenticated.
|
|
73
|
+
- **Reuses existing `kimi-code` sessions.** If you already use the official CLI, the extension reads its session directly. Set `KIMI_CODE_HOME` if its home directory lives somewhere else.
|
|
74
|
+
- **Supports the legacy `~/.kimi` path.** Read-only. Set `KIMI_SHARE_DIR` to override it.
|
|
75
|
+
- **`KIMI_API_KEY` fallback.** For CI or key-based access:
|
|
104
76
|
|
|
105
77
|
```bash
|
|
106
78
|
KIMI_API_KEY=sk-... pi
|
|
107
79
|
```
|
|
108
80
|
|
|
109
|
-
## Models
|
|
81
|
+
## Models (legacy provider registration)
|
|
110
82
|
|
|
111
|
-
|
|
83
|
+
> **New users:** Use Pi's built-in `kimi-coding` provider for model discovery, reasoning controls, and OAuth login. This extension keeps its own model registration only for backward compatibility with existing sessions.
|
|
112
84
|
|
|
113
|
-
|
|
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
|
-
|
|
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.
|
|
85
|
+
When this extension does register models, the official catalog determines what it publishes. The following IDs are the fallback set when catalog discovery is unavailable:
|
|
120
86
|
|
|
121
|
-
|
|
87
|
+
| Pi model ID | Default name | Source |
|
|
88
|
+
| --------------------------------------- | -------------------------- | ---------------- |
|
|
89
|
+
| `kimi-coding/kimi-for-coding` | Kimi for Coding | Official catalog |
|
|
90
|
+
| `kimi-coding/kimi-for-coding-highspeed` | Kimi for Coding High Speed | Official catalog |
|
|
91
|
+
| `kimi-coding/k3` | Kimi K3 | Official catalog |
|
|
122
92
|
|
|
123
|
-
|
|
124
|
-
/model kimi-coding/kimi-for-coding
|
|
125
|
-
/model kimi-coding/kimi-for-coding-highspeed
|
|
126
|
-
/model kimi-coding/k3
|
|
127
|
-
```
|
|
128
|
-
|
|
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.
|
|
93
|
+
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.
|
|
130
94
|
|
|
131
95
|
Fallback values:
|
|
132
96
|
|
|
@@ -141,7 +105,7 @@ Switching models or thinking effort invalidates Kimi's existing context cache. S
|
|
|
141
105
|
|
|
142
106
|
## Optional tools
|
|
143
107
|
|
|
144
|
-
Kimi
|
|
108
|
+
Kimi Code has server-side capabilities that this extension exposes as opt-in tools. All tools are off by default. Enable them individually through `/kimi-settings` or JSON config.
|
|
145
109
|
|
|
146
110
|
Inside Pi, run:
|
|
147
111
|
|
|
@@ -149,7 +113,7 @@ Inside Pi, run:
|
|
|
149
113
|
/kimi-settings
|
|
150
114
|
```
|
|
151
115
|
|
|
152
|
-
That command shows the current server-side model name (e.g. "K2.7 Code High Speed"), your Kimi quota and Extra Usage balance, and lets you edit the home or project config. Changes apply to the active session tool set.
|
|
116
|
+
That command shows your signed-in account (via Kimi's `/me` endpoint), the current server-side model name (e.g. "K2.7 Code High Speed"), your Kimi quota and Extra Usage balance, and lets you edit the home or project config. Changes apply to the active session tool set.
|
|
153
117
|
|
|
154
118
|
Configurable settings include protocol mode, upload threshold, and per-tool enable/collapse.
|
|
155
119
|
|
|
@@ -200,6 +164,11 @@ Supported datasource names:
|
|
|
200
164
|
| `arxiv` | Academic paper search and metadata |
|
|
201
165
|
| `scholar` | Academic literature search |
|
|
202
166
|
| `yuandian_law` | Chinese legal database and case law |
|
|
167
|
+
| `wind` | A-share intraday series, funds, bonds |
|
|
168
|
+
| `imf` | IMF FX, CPI, and GDP forecasts |
|
|
169
|
+
| `gildata` | Natural-language stock/fund screening |
|
|
170
|
+
| `sec_edgar` | US SEC filings (10-K/10-Q, 13F, 8-K) |
|
|
171
|
+
| `sp_data` | S&P fundamentals and transcripts |
|
|
203
172
|
|
|
204
173
|
### Common notes
|
|
205
174
|
|
|
@@ -213,8 +182,8 @@ If you already use another search or fetch tool, pick one path for a session. Ov
|
|
|
213
182
|
|
|
214
183
|
Most users do not need environment variables. Two are worth knowing:
|
|
215
184
|
|
|
216
|
-
- `KIMI_API_KEY` — static API key for CI or
|
|
217
|
-
- `KIMI_CODE_PROTOCOL` — `openai` by default; set to `anthropic`
|
|
185
|
+
- `KIMI_API_KEY` — static API key for CI or key-based access.
|
|
186
|
+
- `KIMI_CODE_PROTOCOL` — `openai` by default; set to `anthropic` for Anthropic-compatible requests.
|
|
218
187
|
|
|
219
188
|
Tools, protocol, and upload threshold are all configurable through `/kimi-settings` or JSON config files.
|
|
220
189
|
|
|
@@ -224,30 +193,15 @@ The full env list, including base URL overrides, `kimi-code` path overrides, upl
|
|
|
224
193
|
|
|
225
194
|
### Kimi Code vs Pi
|
|
226
195
|
|
|
227
|
-
`kimi-code` is Moonshot's official terminal agent. Pi is the harness you adapt to your own workflow
|
|
196
|
+
`kimi-code` is Moonshot's official terminal agent. Pi is the harness you adapt to your own workflow. Pi's built-in `kimi-coding` provider handles the LLM; this extension adds the rest of the Kimi Code surface to Pi.
|
|
228
197
|
|
|
229
198
|
### Cache behavior
|
|
230
199
|
|
|
231
|
-
Kimi's cache is content-based
|
|
232
|
-
|
|
233
|
-
See [docs/caching.md](docs/caching.md) for the measured behavior: TTL, cross-protocol cache reuse, 256-token prefix alignment, device-id sharing, and the cases that invalidate cache.
|
|
200
|
+
Kimi's cache is content-based and keyed purely by prompt prefix. `prompt_cache_key`, Anthropic `cache_control` markers, and device-id headers are ignored. Cache benefits accrue within a session, not across cold starts. See [docs/caching.md](docs/caching.md) for measured behavior.
|
|
234
201
|
|
|
235
202
|
### Protocol modes
|
|
236
203
|
|
|
237
|
-
Kimi's coding endpoint speaks both Anthropic and OpenAI dialects. This extension defaults to OpenAI-compatible mode.
|
|
238
|
-
|
|
239
|
-
`KIMI_CODE_PROTOCOL` supports:
|
|
240
|
-
|
|
241
|
-
- `openai` — default, uses `/coding/v1/chat/completions`
|
|
242
|
-
- `anthropic` — uses `/coding/v1/messages`
|
|
243
|
-
|
|
244
|
-
Use Anthropic mode if your Pi setup depends on Anthropic-style request or tool semantics.
|
|
245
|
-
|
|
246
|
-
### Why OpenAI-compatible mode is the default
|
|
247
|
-
|
|
248
|
-
The Anthropic-compatible protocol has an awkward tool-result shape: tool output is wrapped inside a `role: "user"` message. That is valid Anthropic format, but Kimi can sometimes read it like a real user message and continue from the wrong premise in multi-turn tool loops.
|
|
249
|
-
|
|
250
|
-
The OpenAI-compatible protocol uses a dedicated `role: "tool"` message, so the boundary is clearer. That is why this extension now defaults to `openai`. The Anthropic path stays available for setups that need it. See [issue #5](https://github.com/Leechael/pi-provider-kimi-code/issues/5) for details.
|
|
204
|
+
Kimi's coding endpoint speaks both Anthropic and OpenAI dialects. This extension defaults to OpenAI-compatible mode (`/coding/v1/chat/completions`) because it uses a dedicated `role: "tool"` message, which keeps tool results separate from user messages in multi-turn loops. Anthropic mode (`/coding/v1/messages`) is available if your setup depends on Anthropic-style semantics. See [issue #5](https://github.com/Leechael/pi-provider-kimi-code/issues/5) for details.
|
|
251
205
|
|
|
252
206
|
### Is this affiliated with Moonshot AI?
|
|
253
207
|
|
|
@@ -257,25 +211,19 @@ No. This is an independent extension. The login flow is derived from the public
|
|
|
257
211
|
|
|
258
212
|
### `pi` reports `fetch failed` even though `curl` works
|
|
259
213
|
|
|
260
|
-
Pi
|
|
214
|
+
Pi uses Node's `fetch` / undici stack, which handles `http_proxy` / `https_proxy` / `all_proxy` differently from `curl`. Check the proxy variables in the Pi process's environment. `scripts/test_e2e.sh` prints the effective proxy settings for debugging.
|
|
261
215
|
|
|
262
216
|
### `/login kimi-coding` prints a device code but the browser never opens
|
|
263
217
|
|
|
264
|
-
The login flow always prints a verification URL.
|
|
218
|
+
The login flow always prints a verification URL. Copy it into a browser manually if your terminal or OS blocks auto-open.
|
|
265
219
|
|
|
266
220
|
### "Access denied" or subscription errors after a successful login
|
|
267
221
|
|
|
268
|
-
Your Moonshot account needs an active Kimi Code Plan
|
|
269
|
-
|
|
270
|
-
### Tools do not show up
|
|
271
|
-
|
|
272
|
-
Run `/kimi-settings` and check whether the tool is enabled in the home or project config. All tools also require `/login kimi-coding`; `KIMI_API_KEY` is not enough.
|
|
273
|
-
|
|
274
|
-
If you changed the JSON config by hand, reload it through `/kimi-settings` or start a new Pi session.
|
|
222
|
+
Your Moonshot account needs an active Kimi Code Plan. If the same account works in `kimi-code`, re-run `/login kimi-coding` to refresh credentials.
|
|
275
223
|
|
|
276
|
-
### Tools
|
|
224
|
+
### Tools do not show up or return errors
|
|
277
225
|
|
|
278
|
-
|
|
226
|
+
Run `/kimi-settings` and check whether the tool is enabled in the home or project config. All tools require OAuth login; `KIMI_API_KEY` is not enough. Subscription or whitelist errors come from the upstream Moonshot service.
|
|
279
227
|
|
|
280
228
|
### Large images fail with a payload error
|
|
281
229
|
|
|
@@ -283,19 +231,11 @@ This extension uploads images over `KIMI_CODE_UPLOAD_THRESHOLD_BYTES` (default 1
|
|
|
283
231
|
|
|
284
232
|
### Prompt cache never seems to hit
|
|
285
233
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
- On the Anthropic-compat endpoint, the `system` prompt or the `tools` array changed (e.g. an extension changed the tool set mid-session). Either fully invalidates the cache — see [docs/caching.md](docs/caching.md), Finding 11. OpenAI-compat endpoint not yet measured for this scenario.
|
|
289
|
-
- An `image_url` reference (`ms://<file_id>`) changed between turns. Same rule — see Finding 17.
|
|
290
|
-
- You're comparing across two cold `pi` invocations, not within a single session. Cache benefits accrue **within** a session; Pi's system/tools assembly drifts between sessions. See Finding 18.
|
|
291
|
-
- A timestamp, request ID, or randomized header is being interpolated into the prompt.
|
|
292
|
-
- The first ~256 tokens of the prompt differ between turns.
|
|
293
|
-
|
|
294
|
-
`PI_CACHE_RETENTION=none` skips `prompt_cache_key` injection but **does not** disable Kimi's caching (the cache is unconditional). For deterministic measurement, run a focused suite under `scripts/e2e/cache/`, starting with `scripts/e2e/cache/ttl.sh`. See [docs/caching.md](docs/caching.md) for full mechanics.
|
|
234
|
+
Cache benefits accrue within a single session. If `cache_read_input_tokens` (Anthropic) or `cached_tokens` (OpenAI) stays at `0`, something in the prompt is varying between turns — a changed system prompt, tool set, `ms://` reference, or timestamp. See [docs/caching.md](docs/caching.md) for the full list of invalidation causes and measurement scripts.
|
|
295
235
|
|
|
296
236
|
### OpenAI-compatible tools complain about a `developer` role
|
|
297
237
|
|
|
298
|
-
In OpenAI mode this extension maps
|
|
238
|
+
In OpenAI mode this extension maps `developer` to `system` because Kimi's Coding endpoint does not recognize `developer`. Use Anthropic mode if your toolchain expects `developer` to round-trip.
|
|
299
239
|
|
|
300
240
|
## References
|
|
301
241
|
|
|
@@ -306,10 +246,6 @@ In OpenAI mode this extension maps the `developer` role to `system` (Kimi's Codi
|
|
|
306
246
|
- Cache behavior: [docs/caching.md](docs/caching.md)
|
|
307
247
|
- Architecture notes: [docs/architecture.md](docs/architecture.md)
|
|
308
248
|
|
|
309
|
-
## Credits
|
|
310
|
-
|
|
311
|
-
Based on the login implementation from Kimi Code by Moonshot AI. Pi was originally created by [@badlogic](https://github.com/badlogic).
|
|
312
|
-
|
|
313
249
|
## License
|
|
314
250
|
|
|
315
251
|
MIT
|
package/index.ts
CHANGED
|
@@ -27,6 +27,9 @@ import {
|
|
|
27
27
|
import { Input, SettingsList, type SettingItem, truncateToWidth } from "@earendil-works/pi-tui";
|
|
28
28
|
import os from "node:os";
|
|
29
29
|
|
|
30
|
+
// pi-ai <=0.79 has no "./compat"
|
|
31
|
+
const piAiCompatModule = "@earendil-works/pi-ai/compat";
|
|
32
|
+
|
|
30
33
|
import {
|
|
31
34
|
type KimiCodeConfig,
|
|
32
35
|
type KimiCodeConfigPatch,
|
|
@@ -38,7 +41,13 @@ import {
|
|
|
38
41
|
saveProjectKimiCodeConfig,
|
|
39
42
|
type KimiToolName,
|
|
40
43
|
} from "./src/config.ts";
|
|
41
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
KIMI_GLOBAL_FALLBACK_SOURCE_ID,
|
|
46
|
+
PROVIDER_ID,
|
|
47
|
+
PROVIDER_VERSION,
|
|
48
|
+
getBaseUrl,
|
|
49
|
+
getKimiApiType,
|
|
50
|
+
} from "./src/constants.ts";
|
|
42
51
|
import {
|
|
43
52
|
type KimiOAuthCredentials,
|
|
44
53
|
type KimiOAuthExtras,
|
|
@@ -53,7 +62,12 @@ import {
|
|
|
53
62
|
hasKimiModelMetadata,
|
|
54
63
|
resolveKimiModelConfig,
|
|
55
64
|
} from "./src/models.ts";
|
|
56
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
getKimiApiKey,
|
|
67
|
+
loginKimiCode,
|
|
68
|
+
refreshKimiAuthToken,
|
|
69
|
+
refreshKimiCodeToken,
|
|
70
|
+
} from "./src/oauth.ts";
|
|
57
71
|
import { isKimiProjectConfigApproved } from "./src/project-trust.ts";
|
|
58
72
|
import {
|
|
59
73
|
type KimiConfigScope,
|
|
@@ -63,12 +77,17 @@ import {
|
|
|
63
77
|
parseByteSizeInput,
|
|
64
78
|
} from "./src/settings-ui.ts";
|
|
65
79
|
import { setStoreResolvedKimiConfig, streamSimpleKimi } from "./src/stream.ts";
|
|
66
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
fetchKimiUsageSnapshot,
|
|
82
|
+
fetchKimiUserInfoSnapshot,
|
|
83
|
+
getKimiUsageToken,
|
|
84
|
+
} from "./src/usage.ts";
|
|
67
85
|
import { buildMoonshotFetchTool, buildMoonshotSearchTool } from "./src/tools/moonshot.ts";
|
|
68
86
|
import { buildKimiDatasourceTool } from "./src/tools/datasource.ts";
|
|
69
87
|
interface KimiRuntimeState {
|
|
70
88
|
cwd: string;
|
|
71
89
|
config: KimiCodeConfig;
|
|
90
|
+
fallbackSourceId: string;
|
|
72
91
|
modelExtras: KimiOAuthExtras;
|
|
73
92
|
projectTrusted: boolean;
|
|
74
93
|
overrides?: KimiCodeConfigPatch;
|
|
@@ -141,23 +160,39 @@ function applyEffectiveKimiRuntimeConfig(
|
|
|
141
160
|
return config;
|
|
142
161
|
}
|
|
143
162
|
|
|
144
|
-
async function refreshModelExtras(
|
|
145
|
-
|
|
163
|
+
async function refreshModelExtras(
|
|
164
|
+
state: KimiRuntimeState,
|
|
165
|
+
token = getKimiUsageToken(),
|
|
166
|
+
): Promise<boolean> {
|
|
146
167
|
if (!token) return false;
|
|
147
|
-
const extras = await discoverKimiModelMetadata(token, state.config.protocol
|
|
168
|
+
const extras = await discoverKimiModelMetadata(token, state.config.protocol, {
|
|
169
|
+
refreshAccessToken: refreshKimiAuthToken,
|
|
170
|
+
});
|
|
148
171
|
if (Object.keys(extras).length === 0) return false;
|
|
149
172
|
Object.assign(state.modelExtras, extras);
|
|
150
173
|
return true;
|
|
151
174
|
}
|
|
152
175
|
|
|
176
|
+
// Discovery can lose a race with a concurrent OAuth refresh: the token it
|
|
177
|
+
// started with may already be dead by the time the request goes out. Retry
|
|
178
|
+
// once, but only when the stored token actually changed underneath us.
|
|
179
|
+
async function discoverModelExtras(state: KimiRuntimeState): Promise<boolean> {
|
|
180
|
+
const token = getKimiUsageToken();
|
|
181
|
+
if (await refreshModelExtras(state, token)) return true;
|
|
182
|
+
const refreshedToken = getKimiUsageToken();
|
|
183
|
+
if (!refreshedToken || refreshedToken === token) return false;
|
|
184
|
+
return refreshModelExtras(state, refreshedToken);
|
|
185
|
+
}
|
|
186
|
+
|
|
153
187
|
async function openSettingsMenu(
|
|
154
188
|
pi: ExtensionAPI,
|
|
155
189
|
ctx: ExtensionCommandContext,
|
|
156
190
|
state: KimiRuntimeState,
|
|
157
191
|
): Promise<void> {
|
|
158
192
|
const modelDiscoveryToken = getKimiUsageToken();
|
|
159
|
-
const [usageSnapshot, initialModelsRefreshed] = await Promise.all([
|
|
193
|
+
const [usageSnapshot, userInfoSnapshot, initialModelsRefreshed] = await Promise.all([
|
|
160
194
|
fetchKimiUsageSnapshot(),
|
|
195
|
+
fetchKimiUserInfoSnapshot(),
|
|
161
196
|
refreshModelExtras(state),
|
|
162
197
|
]);
|
|
163
198
|
let modelsRefreshed = initialModelsRefreshed;
|
|
@@ -165,8 +200,9 @@ async function openSettingsMenu(
|
|
|
165
200
|
if (!modelsRefreshed && refreshedToken && refreshedToken !== modelDiscoveryToken) {
|
|
166
201
|
modelsRefreshed = await refreshModelExtras(state);
|
|
167
202
|
}
|
|
168
|
-
if (modelsRefreshed) registerKimiProvider(pi, state);
|
|
203
|
+
if (modelsRefreshed) await registerKimiProvider(pi, state);
|
|
169
204
|
const usage = usageSnapshot.summary;
|
|
205
|
+
const account = userInfoSnapshot.summary;
|
|
170
206
|
|
|
171
207
|
const projectTrusted = await isKimiProjectConfigApproved(ctx, ctx.cwd);
|
|
172
208
|
const homeDraft = loadHomeKimiCodeConfig(os.homedir());
|
|
@@ -343,6 +379,9 @@ async function openSettingsMenu(
|
|
|
343
379
|
width,
|
|
344
380
|
),
|
|
345
381
|
"",
|
|
382
|
+
truncateToWidth(theme.fg("accent", theme.bold("Kimi account")), width),
|
|
383
|
+
truncateToWidth(` ${account}`, width),
|
|
384
|
+
"",
|
|
346
385
|
truncateToWidth(theme.fg("accent", theme.bold("Kimi usage")), width),
|
|
347
386
|
...usageLines,
|
|
348
387
|
"",
|
|
@@ -402,7 +441,94 @@ function buildKimiCatalogModels(state: KimiRuntimeState) {
|
|
|
402
441
|
);
|
|
403
442
|
}
|
|
404
443
|
|
|
405
|
-
|
|
444
|
+
// Node reports a missing subpath export (pi-ai <=0.79) and a missing package
|
|
445
|
+
// with these codes. All mean "this pi-ai has no compat layer", which is an
|
|
446
|
+
// expected configuration rather than a failure worth reporting: the fallback
|
|
447
|
+
// simply does not exist there.
|
|
448
|
+
//
|
|
449
|
+
// MODULE_NOT_FOUND (the CJS code, without the ERR_ prefix) covers pi's jiti
|
|
450
|
+
// extension loader on old pi versions: it aliases the bare "@earendil-works/pi-ai"
|
|
451
|
+
// specifier straight to dist/index.js, so the "/compat" subpath degrades into
|
|
452
|
+
// the bogus file path "dist/index.js/compat" and fails legacy CJS resolution
|
|
453
|
+
// instead of exports-map resolution.
|
|
454
|
+
const MODULE_UNAVAILABLE_CODES = new Set([
|
|
455
|
+
"ERR_MODULE_NOT_FOUND",
|
|
456
|
+
"ERR_PACKAGE_PATH_NOT_EXPORTED",
|
|
457
|
+
"MODULE_NOT_FOUND",
|
|
458
|
+
]);
|
|
459
|
+
|
|
460
|
+
// Every api id this extension can hand out, not only the one for the currently
|
|
461
|
+
// configured protocol: sessions persist the api id that was current when the
|
|
462
|
+
// model was resolved, so a session created before a protocol switch still
|
|
463
|
+
// carries the other id and would keep crashing. streamSimpleKimi routes on the
|
|
464
|
+
// model's wireProtocol / the resolved runtime config and never on model.api, so
|
|
465
|
+
// both ids dispatch identically.
|
|
466
|
+
const KIMI_FALLBACK_APIS = [getKimiApiType("openai"), getKimiApiType("anthropic")];
|
|
467
|
+
|
|
468
|
+
type PiAiApiRegistry = {
|
|
469
|
+
registerApiProvider: (
|
|
470
|
+
provider: {
|
|
471
|
+
api: string;
|
|
472
|
+
stream: typeof streamSimpleKimi;
|
|
473
|
+
streamSimple: typeof streamSimpleKimi;
|
|
474
|
+
},
|
|
475
|
+
sourceId: string,
|
|
476
|
+
) => void;
|
|
477
|
+
unregisterApiProviders: (sourceId: string) => void;
|
|
478
|
+
getApiProvider: (api: string) => unknown;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
async function loadPiAiApiRegistry(action: string): Promise<PiAiApiRegistry | null> {
|
|
482
|
+
try {
|
|
483
|
+
return (await import(piAiCompatModule)) as PiAiApiRegistry;
|
|
484
|
+
} catch (error: unknown) {
|
|
485
|
+
const code = (error as { code?: unknown } | undefined)?.code;
|
|
486
|
+
if (typeof code === "string" && MODULE_UNAVAILABLE_CODES.has(code)) return null;
|
|
487
|
+
console.error(
|
|
488
|
+
`[pi-provider-kimi-code] failed to ${action} the global api fallback:`,
|
|
489
|
+
error instanceof Error ? error.message : error,
|
|
490
|
+
);
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
let nextFallbackSourceId = 0;
|
|
496
|
+
const liveFallbackSources = new Set<string>();
|
|
497
|
+
|
|
498
|
+
function createKimiGlobalFallbackSourceId(): string {
|
|
499
|
+
nextFallbackSourceId += 1;
|
|
500
|
+
return `${KIMI_GLOBAL_FALLBACK_SOURCE_ID}:${nextFallbackSourceId}`;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function registerKimiGlobalApiFallback(sourceId: string): Promise<void> {
|
|
504
|
+
if (!liveFallbackSources.has(sourceId)) return;
|
|
505
|
+
const registry = await loadPiAiApiRegistry("register");
|
|
506
|
+
if (!registry) return;
|
|
507
|
+
for (const api of KIMI_FALLBACK_APIS) {
|
|
508
|
+
// Never displace an entry someone else owns: this is a shared,
|
|
509
|
+
// process-wide table and the fallback only exists to fill a hole, not to
|
|
510
|
+
// win a conflict. pi-ai installs its own builtins under the same rule.
|
|
511
|
+
if (registry.getApiProvider(api)) continue;
|
|
512
|
+
registry.registerApiProvider(
|
|
513
|
+
{ api, stream: streamSimpleKimi, streamSimple: streamSimpleKimi },
|
|
514
|
+
sourceId,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// The registry outlives each session. Remove only the entries owned by the
|
|
520
|
+
// retiring runtime, then hand any holes to another live runtime.
|
|
521
|
+
async function unregisterKimiGlobalApiFallback(sourceId: string): Promise<void> {
|
|
522
|
+
if (!liveFallbackSources.delete(sourceId)) return;
|
|
523
|
+
const registry = await loadPiAiApiRegistry("unregister");
|
|
524
|
+
if (!registry) return;
|
|
525
|
+
registry.unregisterApiProviders(sourceId);
|
|
526
|
+
const successor = liveFallbackSources.values().next().value;
|
|
527
|
+
if (successor) await registerKimiGlobalApiFallback(successor);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function registerKimiProvider(pi: ExtensionAPI, state: KimiRuntimeState): Promise<void> {
|
|
531
|
+
await registerKimiGlobalApiFallback(state.fallbackSourceId);
|
|
406
532
|
pi.registerProvider(PROVIDER_ID, {
|
|
407
533
|
baseUrl: getBaseUrl(state.config.protocol),
|
|
408
534
|
apiKey: "$KIMI_API_KEY",
|
|
@@ -481,6 +607,50 @@ function registerKimiProvider(pi: ExtensionAPI, state: KimiRuntimeState): void {
|
|
|
481
607
|
});
|
|
482
608
|
}
|
|
483
609
|
|
|
610
|
+
// What pi throws from any `pi` method once the extension runtime behind it has
|
|
611
|
+
// been retired, which is what a /reload does. Same wording on every version
|
|
612
|
+
// this extension supports (0.78.1 through 0.83.0); if it ever changes, the
|
|
613
|
+
// worst case is that a reload race gets logged instead of ignored.
|
|
614
|
+
const STALE_EXTENSION_RUNTIME_PREFIX = "This extension ctx is stale";
|
|
615
|
+
|
|
616
|
+
let pendingModelDiscovery: Promise<void> = Promise.resolve();
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Resolves once the discovery started by the most recent extension load has
|
|
620
|
+
* settled. Nothing in the runtime waits for it; it exists so tests can observe
|
|
621
|
+
* the deferred registration without polling.
|
|
622
|
+
*/
|
|
623
|
+
export function kimiModelDiscoverySettled(): Promise<void> {
|
|
624
|
+
return pendingModelDiscovery;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// pi's extension loader awaits this factory before it builds the session, and
|
|
628
|
+
// the TUI only draws its first frame after that returns, so a network round
|
|
629
|
+
// trip here is a stall the user sees on every launch and every /reload. The
|
|
630
|
+
// stored OAuth credential already carries the last known catalog (pi replays it
|
|
631
|
+
// through oauth.modifyModels), so register what the config describes right away
|
|
632
|
+
// and fold the fresh metadata in when it lands.
|
|
633
|
+
function startModelDiscovery(pi: ExtensionAPI, state: KimiRuntimeState): void {
|
|
634
|
+
pendingModelDiscovery = discoverModelExtras(state)
|
|
635
|
+
.then((discovered) => {
|
|
636
|
+
if (!discovered) return;
|
|
637
|
+
reloadEffectiveKimiRuntimeConfig(state, state.cwd, state.projectTrusted);
|
|
638
|
+
return registerKimiProvider(pi, state);
|
|
639
|
+
})
|
|
640
|
+
.catch((error: unknown) => {
|
|
641
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
642
|
+
// A /reload retires this extension runtime while discovery may still be
|
|
643
|
+
// in flight, and registering against a retired runtime throws. The
|
|
644
|
+
// reloaded factory runs its own discovery, so there is nothing to redo.
|
|
645
|
+
if (message.startsWith(STALE_EXTENSION_RUNTIME_PREFIX)) return;
|
|
646
|
+
// Anything else used to surface as an extension load failure, back when
|
|
647
|
+
// this ran inside the factory. Deferring it must not make it invisible:
|
|
648
|
+
// the provider stays registered with the metadata it already had, and
|
|
649
|
+
// the user needs to know why it stopped refreshing.
|
|
650
|
+
console.error("[kimi-coding] deferred model discovery failed:", message);
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
484
654
|
export function KimiCode(overrides?: KimiCodeConfigPatch): ExtensionFactory {
|
|
485
655
|
return async (pi: ExtensionAPI) => {
|
|
486
656
|
const cwd = process.cwd();
|
|
@@ -488,28 +658,22 @@ export function KimiCode(overrides?: KimiCodeConfigPatch): ExtensionFactory {
|
|
|
488
658
|
{ cwd, home: os.homedir(), includeProject: false },
|
|
489
659
|
overrides,
|
|
490
660
|
);
|
|
491
|
-
const discoveryToken = getKimiUsageToken();
|
|
492
|
-
const initialDiscovery = discoveryToken
|
|
493
|
-
? await discoverKimiModelMetadata(discoveryToken, config.protocol)
|
|
494
|
-
: {};
|
|
495
|
-
let discovered = initialDiscovery;
|
|
496
|
-
const refreshedToken = getKimiUsageToken();
|
|
497
|
-
if (
|
|
498
|
-
Object.keys(discovered).length === 0 &&
|
|
499
|
-
refreshedToken &&
|
|
500
|
-
refreshedToken !== discoveryToken
|
|
501
|
-
) {
|
|
502
|
-
discovered = await discoverKimiModelMetadata(refreshedToken, config.protocol);
|
|
503
|
-
}
|
|
504
661
|
const state: KimiRuntimeState = {
|
|
505
662
|
cwd,
|
|
506
663
|
config,
|
|
507
|
-
|
|
664
|
+
fallbackSourceId: createKimiGlobalFallbackSourceId(),
|
|
665
|
+
modelExtras: {},
|
|
508
666
|
projectTrusted: false,
|
|
509
667
|
overrides,
|
|
510
668
|
};
|
|
511
669
|
reloadEffectiveKimiRuntimeConfig(state, cwd, false);
|
|
512
|
-
|
|
670
|
+
liveFallbackSources.add(state.fallbackSourceId);
|
|
671
|
+
try {
|
|
672
|
+
await registerKimiProvider(pi, state);
|
|
673
|
+
} catch (error) {
|
|
674
|
+
liveFallbackSources.delete(state.fallbackSourceId);
|
|
675
|
+
throw error;
|
|
676
|
+
}
|
|
513
677
|
|
|
514
678
|
registerConfiguredMoonshotTools(pi, state.config, { updateActiveTools: false });
|
|
515
679
|
|
|
@@ -519,7 +683,11 @@ export function KimiCode(overrides?: KimiCodeConfigPatch): ExtensionFactory {
|
|
|
519
683
|
updateActiveTools: true,
|
|
520
684
|
projectTrusted,
|
|
521
685
|
});
|
|
522
|
-
registerKimiProvider(pi, state);
|
|
686
|
+
await registerKimiProvider(pi, state);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
pi.on("session_shutdown", async () => {
|
|
690
|
+
await unregisterKimiGlobalApiFallback(state.fallbackSourceId);
|
|
523
691
|
});
|
|
524
692
|
|
|
525
693
|
pi.registerCommand("kimi-settings", {
|
|
@@ -532,6 +700,8 @@ export function KimiCode(overrides?: KimiCodeConfigPatch): ExtensionFactory {
|
|
|
532
700
|
await openSettingsMenu(pi, ctx, state);
|
|
533
701
|
},
|
|
534
702
|
});
|
|
703
|
+
|
|
704
|
+
startModelDiscovery(pi, state);
|
|
535
705
|
};
|
|
536
706
|
}
|
|
537
707
|
|
package/package.json
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-provider-kimi-code",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.10",
|
|
4
|
+
"description": "Kimi Code models (K3, K2.7, HighSpeed) in Pi: OAuth shared login with kimi-code CLI, file uploads, and Kimi-native tools",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-provider",
|
|
7
7
|
"coding-agent",
|
|
8
|
+
"highspeed",
|
|
9
|
+
"k3",
|
|
8
10
|
"kimi",
|
|
9
11
|
"kimi-code",
|
|
12
|
+
"kimi-code-plan",
|
|
10
13
|
"kimi-for-coding",
|
|
11
14
|
"kimi-k2",
|
|
12
15
|
"kimi-k2.5",
|
|
13
16
|
"kimi-k2.6",
|
|
14
17
|
"kimi-k2.7",
|
|
18
|
+
"kimi-k3",
|
|
15
19
|
"llm",
|
|
16
20
|
"llm-provider",
|
|
17
21
|
"moonshot",
|
|
@@ -53,9 +57,9 @@
|
|
|
53
57
|
"proper-lockfile": "^4.1.2"
|
|
54
58
|
},
|
|
55
59
|
"devDependencies": {
|
|
56
|
-
"@earendil-works/pi-ai": "^0.
|
|
57
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
58
|
-
"@earendil-works/pi-tui": "^0.
|
|
60
|
+
"@earendil-works/pi-ai": "^0.83.0",
|
|
61
|
+
"@earendil-works/pi-coding-agent": "^0.83.0",
|
|
62
|
+
"@earendil-works/pi-tui": "^0.83.0",
|
|
59
63
|
"@j178/prek": "^0.4.4",
|
|
60
64
|
"@types/node": "^24.0.0",
|
|
61
65
|
"@types/proper-lockfile": "^4.1.4",
|