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 CHANGED
@@ -3,40 +3,29 @@
3
3
  [![npm](https://img.shields.io/npm/v/pi-provider-kimi-code)](https://www.npmjs.com/package/pi-provider-kimi-code)
4
4
  [![license](https://img.shields.io/npm/l/pi-provider-kimi-code)](./LICENSE)
5
5
 
6
- **Use Kimi Code in Pi, with the Kimi parts handled.**
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
- Pi is a small harness you adapt to your own workflow. Kimi Code is Moonshot's official coding agent. This package sits between them: Kimi Code as a Pi provider, plus the Kimi-specific details that the basic provider keeps simple.
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
- - **Use your Kimi account.** Log in with `/login kimi-coding`, or reuse an existing `kimi-code` session.
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
- - **Know what the cache is doing.** Kimi caches by content prefix. This repo measures that behavior instead of pretending `prompt_cache_key` controls it.
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
- - **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
- - **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
- - Kimi account login in Pi, plus `KIMI_API_KEY` for CI or pay-per-token use.
29
- - `kimi-code` credential reuse from `~/.kimi-code/credentials/kimi-code.json`, with read-only support for the legacy `~/.kimi` path.
30
- - Kimi Files API uploads for large inline images.
31
- - Live model metadata from Kimi's official catalog, including server-advertised model availability and context limits.
32
- - OpenAI-compatible mode by default, Anthropic-compatible mode on request.
33
- - K2.7 and K3 reasoning level mapping for Pi's reasoning controls.
34
- - Tool schema dedup to stay under Moonshot's 15 KB per-tool-schema limit.
35
- - K2.7 parameter guard: removes rejected `temperature` and `top_p` values and rewrites unsupported `tool_choice` values to `auto`.
36
- - Stream cleanup for Kimi's thinking-only placeholder text.
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
- ## Sign in
80
-
81
- Inside Pi, run:
82
-
83
- ```text
84
- /login kimi-coding
85
- ```
68
+ ## Authentication
86
69
 
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:
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
- ```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:
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
- The official catalog determines the models this provider publishes. The following IDs are the fallback set when catalog discovery is unavailable:
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
- | Pi model ID | Default name | Availability and context |
114
- | --------------------------------------- | ------------------------ | ------------------------ |
115
- | `kimi-coding/kimi-for-coding` | Kimi K2.7 Code | Official catalog |
116
- | `kimi-coding/kimi-for-coding-highspeed` | Kimi K2.7 Code HighSpeed | Official catalog |
117
- | `kimi-coding/k3` | Kimi K3 | Official catalog |
118
-
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
- Select a model inside Pi:
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
- ```text
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 Coding has server-side capabilities that this extension can expose as opt-in tools. All tools are off by default. Enable them individually through `/kimi-settings` or JSON config.
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 pay-per-token use.
217
- - `KIMI_CODE_PROTOCOL` — `openai` by default; set to `anthropic` if your Pi setup needs Anthropic-compatible requests. Can also be set through `/kimi-settings` or JSON config.
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: extensions, skills, prompts, themes, custom commands, status bars, and UI pieces. This package lets you keep Kimi Code as the model path while staying inside Pi.
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. It fires automatically when your prompt prefix matches an earlier request. `prompt_cache_key`, Anthropic `cache_control` markers, and identifying headers like `X-Msh-Device-Id` are all ignored for cache decisions — cache is keyed purely by prompt content and is shared across machines, agent forks, and device-id rotations on the same Kimi account.
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 runs on Node's `fetch` / undici stack, which handles `http_proxy` / `https_proxy` / `all_proxy` differently from `curl`. Verify those variables in the Pi process's environment. The bundled smoke-test script `scripts/test_e2e.sh` prints the effective proxy-related environment for easier debugging.
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. If your terminal or OS blocks auto-open, copy the URL and paste it into a browser manually.
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 for the provider to do anything useful. If the same account works in `kimi-code`, re-run `/login kimi-coding` to refresh credentials.
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 return subscription or whitelist errors
224
+ ### Tools do not show up or return errors
277
225
 
278
- The tools call Moonshot's server-side services. Some accounts may not have access even with an active Kimi Code Plan. The error comes from the upstream service.
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
- Kimi's cache is **content-based**: it fires automatically when your prompt prefix matches an earlier request, independent of any explicit cache key. If `cache_read_input_tokens` (Anthropic) or `cached_tokens` (OpenAI) stays at `0` across calls, something in the prompt is varying between turns. The usual suspects:
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 the `developer` role to `system` (Kimi's Coding endpoint does not recognize `developer`). If something in your toolchain expects `developer` to round-trip, use Anthropic mode instead.
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 { PROVIDER_ID, PROVIDER_VERSION, getBaseUrl, getKimiApiType } from "./src/constants.ts";
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 { getKimiApiKey, loginKimiCode, refreshKimiCodeToken } from "./src/oauth.ts";
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 { fetchKimiUsageSnapshot, getKimiUsageToken } from "./src/usage.ts";
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(state: KimiRuntimeState): Promise<boolean> {
145
- const token = getKimiUsageToken();
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
- function registerKimiProvider(pi: ExtensionAPI, state: KimiRuntimeState): void {
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
- modelExtras: discovered,
664
+ fallbackSourceId: createKimiGlobalFallbackSourceId(),
665
+ modelExtras: {},
508
666
  projectTrusted: false,
509
667
  overrides,
510
668
  };
511
669
  reloadEffectiveKimiRuntimeConfig(state, cwd, false);
512
- registerKimiProvider(pi, state);
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.8",
4
- "description": "Use Kimi Code in Pi without losing the Kimi-specific bits",
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.80.9",
57
- "@earendil-works/pi-coding-agent": "^0.80.9",
58
- "@earendil-works/pi-tui": "^0.80.9",
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",