pi-provider-kimi-code 0.5.1 → 0.6.0

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
@@ -5,28 +5,32 @@
5
5
 
6
6
  **Use Kimi Code in Pi, with the Kimi parts handled.**
7
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, measured cache behavior, live model metadata, and optional Moonshot search/fetch tools.
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, measured cache behavior, live model metadata, and optional Moonshot search/fetch/datasource tools.
9
+
10
+ > **Kimi K2.7 supported.** The provider automatically discovers the current model from Kimi's `/v1/models` endpoint at login time, so new rollouts (including K2.7) are picked up immediately without a package update.
9
11
 
10
12
  ## Why this exists
11
13
 
12
- Pi is a small harness you adapt to your own workflow. Kimi CLI is Moonshot's official Kimi Code path. This package sits between them: Kimi Code as a Pi provider, plus the Kimi-specific details that the basic provider keeps simple.
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.
13
17
 
14
- - **Use your Kimi account.** Log in with `/login kimi-coding`, or reuse an existing `kimi-cli` session.
18
+ - **Use your Kimi account.** Log in with `/login kimi-coding`, or reuse an existing `kimi-code` session.
15
19
  - **Send files the Kimi way.** Large inline images go through Kimi's Files API and become `ms://` references instead of huge base64 payloads.
16
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.
17
21
  - **Use the safer protocol by default.** OpenAI-compatible mode is the default because tool results are less ambiguous there. Anthropic-compatible mode is still available.
18
- - **Turn on Kimi-native tools when you want them.** `moonshot_search` and `moonshot_fetch` are opt-in, configurable per user or per project.
22
+ - **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.
19
23
 
20
24
  ## What this package adds
21
25
 
22
26
  - Kimi account login in Pi, plus `KIMI_API_KEY` for CI or pay-per-token use.
23
- - `kimi-cli` credential reuse from `~/.kimi/credentials/kimi-code.json`.
27
+ - `kimi-code` credential reuse from `~/.kimi/credentials/kimi-code.json`.
24
28
  - Kimi Files API uploads for large inline images.
25
29
  - Live model metadata from Kimi at login / refresh time.
26
30
  - OpenAI-compatible mode by default, Anthropic-compatible mode on request.
27
31
  - Kimi K2.6 reasoning level mapping for Pi's reasoning controls.
28
32
  - Stream cleanup for Kimi's thinking-only placeholder text.
29
- - Optional `moonshot_search` and `moonshot_fetch` tools via `/kimi-settings`.
33
+ - Optional `moonshot_search`, `moonshot_fetch`, and unified datasource `kimi_datasource` tools via `/kimi-settings`.
30
34
  - No build step; Pi loads the TypeScript extension directly.
31
35
 
32
36
  ## Install
@@ -62,13 +66,13 @@ Inside Pi, run:
62
66
 
63
67
  A browser tab opens, you sign into your Kimi account, and Pi stores the credential at `~/.pi/agent/auth.json`. Tokens refresh automatically.
64
68
 
65
- If you already use `kimi-cli`, this extension can reuse its credential file:
69
+ If you already use `kimi-code`, this extension can reuse its credential file:
66
70
 
67
71
  ```text
68
72
  ~/.kimi/credentials/kimi-code.json
69
73
  ```
70
74
 
71
- The file is read-only from this extension's side. Set `KIMI_SHARE_DIR` if your `kimi-cli` share directory lives somewhere else.
75
+ The file is read-only from this extension's side. Set `KIMI_SHARE_DIR` if your `kimi-code` share directory lives somewhere else.
72
76
 
73
77
  For CI or pay-per-token use, set `KIMI_API_KEY` instead:
74
78
 
@@ -90,32 +94,20 @@ Select it inside Pi:
90
94
  /model kimi-coding/kimi-for-coding
91
95
  ```
92
96
 
93
- 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 or internal test, Pi can pick up the latest model name and context size without waiting for a package release.
97
+ 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.
94
98
 
95
99
  Fallback values:
96
100
 
97
- - Context window: `262144` tokens
98
- - Max output: `32000` tokens
101
+ - Context window: `256k` tokens
102
+ - Max output: `32k` tokens
99
103
  - Input: text and image
100
104
  - Reasoning: enabled
101
105
 
102
- Kimi K2.6 supports Pi's reasoning levels. This provider maps them to Kimi's current reasoning efforts:
103
-
104
- - `off` / `none` — reasoning disabled
105
- - `minimal` / `low` — low effort
106
- - `medium` — medium effort
107
- - `high` / `xhigh` — high effort
108
-
109
- If Kimi reports a larger context window, the provider uses that value. You can also override the advertised context or model name with `KIMI_MODEL_MAX_CONTEXT_SIZE` / `KIMI_MODEL_NAME`; see [docs/ENV.md](docs/ENV.md).
106
+ The provider maps Pi's reasoning levels to Kimi's reasoning-effort controls. The exact mapping depends on the model returned by the server at login time, and the provider refreshes it automatically on every credential refresh.
110
107
 
111
- ## Optional Moonshot tools
108
+ ## Optional tools
112
109
 
113
- Kimi Coding has server-side search and fetch tools. This extension can expose them to Pi as:
114
-
115
- - `moonshot_search`
116
- - `moonshot_fetch`
117
-
118
- They are off by default. Turn them on when you want Kimi's own search/fetch path in the agent loop.
110
+ 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.
119
111
 
120
112
  Inside Pi, run:
121
113
 
@@ -130,18 +122,50 @@ Config files are JSON:
130
122
  - Home: `~/.pi/pi-provider-kimi-code.json`
131
123
  - Project override: `<cwd>/.pi/pi-provider-kimi-code.json`
132
124
 
133
- Project config overrides home config with a deep merge. Missing files or missing keys mean both tools stay off.
125
+ Project config overrides home config with a deep merge. Missing files or missing keys mean all tools stay off.
126
+
127
+ ### Available tools
128
+
129
+ | Tool | Description |
130
+ | ----------------- | ---------------------------------------------- |
131
+ | `moonshot_search` | Web search through Kimi's Moonshot service |
132
+ | `moonshot_fetch` | Web page fetch through Kimi's Moonshot service |
133
+ | `kimi_datasource` | Unified Kimi datasource tool |
134
+
135
+ ### Example config
134
136
 
135
137
  ```json
136
138
  {
137
139
  "tools": {
138
140
  "moonshot_search": { "enabled": true, "default_collapsed": true },
139
- "moonshot_fetch": { "enabled": true, "default_collapsed": true }
141
+ "moonshot_fetch": { "enabled": true, "default_collapsed": true },
142
+ "kimi_datasource": { "enabled": true, "default_collapsed": true }
140
143
  }
141
144
  }
142
145
  ```
143
146
 
144
- Both tools require `/login kimi-coding` OAuth credentials. `KIMI_API_KEY` is not used for these tools. Your account also needs access to Moonshot's server-side search/fetch services; if not, the upstream service can return subscription or whitelist errors.
147
+ ### Datasource tool
148
+
149
+ `kimi_datasource` calls Kimi's `/tools` endpoint. It accepts `data_source_name` plus optional `api_name` and `params`:
150
+
151
+ - Without `api_name`: returns the datasource description (available APIs).
152
+ - With `api_name` and `params`: calls the specified API.
153
+
154
+ Supported datasource names:
155
+
156
+ | Datasource | Description |
157
+ | ---------------------- | ------------------------------------- |
158
+ | `stock_finance_data` | Chinese stock and financial data |
159
+ | `yahoo_finance` | Yahoo Finance market data |
160
+ | `world_bank_open_data` | World Bank open statistics |
161
+ | `tianyancha` | Chinese enterprise and corporate data |
162
+ | `arxiv` | Academic paper search and metadata |
163
+ | `scholar` | Academic literature search |
164
+ | `yuandian_law` | Chinese legal database and case law |
165
+
166
+ ### Common notes
167
+
168
+ All tools require `/login kimi-coding` OAuth credentials. `KIMI_API_KEY` is not used for these tools. Your account also needs access to Moonshot's server-side services; if not, the upstream service can return subscription or whitelist errors.
145
169
 
146
170
  `default_collapsed` controls only the TUI preview. The full tool result still goes to the model; set it to `false` if you want previews expanded by default.
147
171
 
@@ -156,13 +180,13 @@ Most users do not need environment variables. Two are worth knowing:
156
180
 
157
181
  Moonshot search/fetch tools are configured through `/kimi-settings` or JSON config files, not env vars.
158
182
 
159
- The full env list, including base URL overrides, `kimi-cli` path overrides, upload tuning, debug logs, and model metadata overrides, lives in [docs/ENV.md](docs/ENV.md).
183
+ The full env list, including base URL overrides, `kimi-code` path overrides, upload tuning, debug logs, and model metadata overrides, lives in [docs/ENV.md](docs/ENV.md).
160
184
 
161
185
  ## Notes
162
186
 
163
- ### Kimi CLI vs Pi
187
+ ### Kimi Code vs Pi
164
188
 
165
- `kimi-cli` 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.
189
+ `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.
166
190
 
167
191
  ### Cache behavior
168
192
 
@@ -189,7 +213,7 @@ The OpenAI-compatible protocol uses a dedicated `role: "tool"` message, so the b
189
213
 
190
214
  ### Is this affiliated with Moonshot AI?
191
215
 
192
- No. This is an independent extension. The login flow is derived from the public implementation in the open-source [`kimi-cli`](https://github.com/MoonshotAI/kimi-cli) repository.
216
+ No. This is an independent extension. The login flow is derived from the public implementation in the open-source Kimi Code repository.
193
217
 
194
218
  ## Troubleshooting
195
219
 
@@ -203,17 +227,17 @@ The login flow always prints a verification URL. If your terminal or OS blocks a
203
227
 
204
228
  ### "Access denied" or subscription errors after a successful login
205
229
 
206
- Your Moonshot account needs an active Kimi Code subscription for the provider to do anything useful. If the same account works in `kimi-cli`, re-run `/login kimi-coding` to refresh credentials.
230
+ 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.
207
231
 
208
- ### Moonshot tools do not show up
232
+ ### Tools do not show up
209
233
 
210
- Run `/kimi-settings` and check whether `moonshot_search` / `moonshot_fetch` are enabled in the home or project config. These tools also require `/login kimi-coding`; `KIMI_API_KEY` is not enough.
234
+ 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.
211
235
 
212
236
  If you changed the JSON config by hand, reload it through `/kimi-settings` or start a new Pi session.
213
237
 
214
- ### Moonshot tools return subscription or whitelist errors
238
+ ### Tools return subscription or whitelist errors
215
239
 
216
- The tools call Moonshot's server-side search/fetch services. Some accounts may not have access even with an active Kimi Code Plan. The error comes from the upstream service.
240
+ 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.
217
241
 
218
242
  ### Large images fail with a payload error
219
243
 
@@ -238,7 +262,6 @@ In OpenAI mode this extension maps the `developer` role to `system` (Kimi's Codi
238
262
  ## References
239
263
 
240
264
  - Pi: [earendil-works/pi](https://github.com/earendil-works/pi)
241
- - Upstream login implementation and feature request: [MoonshotAI/kimi-cli](https://github.com/MoonshotAI/kimi-cli) · [kimi-cli#757](https://github.com/MoonshotAI/kimi-cli/issues/757)
242
265
  - Environment variables: [docs/ENV.md](docs/ENV.md)
243
266
  - Testing guide: [docs/TESTING.md](docs/TESTING.md)
244
267
  - Cache behavior: [docs/caching.md](docs/caching.md)
@@ -246,7 +269,7 @@ In OpenAI mode this extension maps the `developer` role to `system` (Kimi's Codi
246
269
 
247
270
  ## Credits
248
271
 
249
- Based on the login implementation from [`kimi-cli`](https://github.com/MoonshotAI/kimi-cli) by Moonshot AI. Built as a Pi extension by [@badlogic](https://github.com/badlogic).
272
+ Based on the login implementation from Kimi Code by Moonshot AI. Built as a Pi extension by [@badlogic](https://github.com/badlogic).
250
273
 
251
274
  ## License
252
275
 
package/index.ts CHANGED
@@ -29,13 +29,16 @@ import { relative } from "node:path";
29
29
 
30
30
  import {
31
31
  type KimiCodeConfig,
32
+ KIMI_TOOL_NAMES,
32
33
  getProjectKimiCodeConfigPath,
33
34
  getGlobalKimiCodeConfigPath,
34
35
  loadHomeKimiCodeConfig,
35
36
  loadKimiCodeConfig,
37
+ loadKimiCodeConfigSources,
36
38
  loadProjectKimiCodeConfig,
37
39
  saveHomeKimiCodeConfig,
38
40
  saveProjectKimiCodeConfig,
41
+ type KimiToolName,
39
42
  } from "./src/config.ts";
40
43
  import {
41
44
  DEFAULT_KIMI_MODEL_INPUT,
@@ -49,12 +52,10 @@ import {
49
52
  applyKimiEnvOverridesToModel,
50
53
  applyKimiOAuthExtrasToModel,
51
54
  } from "./src/models.ts";
52
- import { loginKimiCode, refreshKimiCodeToken } from "./src/oauth.ts";
55
+ import { loginKimiCode, refreshKimiAuthToken, refreshKimiCodeToken } from "./src/oauth.ts";
53
56
  import { streamSimpleKimi } from "./src/stream.ts";
54
57
  import { buildMoonshotFetchTool, buildMoonshotSearchTool } from "./src/tools/moonshot.ts";
55
-
56
- const MOONSHOT_TOOL_NAMES = ["moonshot_search", "moonshot_fetch"] as const;
57
- type MoonshotToolName = (typeof MOONSHOT_TOOL_NAMES)[number];
58
+ import { buildKimiDatasourceTool } from "./src/tools/datasource.ts";
58
59
  type KimiConfigScope = "project" | "home";
59
60
  const MEMBERSHIP_LEVEL_NAMES: Record<string, string> = {
60
61
  LEVEL_FREE: "Free",
@@ -71,30 +72,30 @@ interface UsageRow {
71
72
  limit: number;
72
73
  }
73
74
 
75
+ function buildKimiTool(toolName: KimiToolName, config: KimiCodeConfig) {
76
+ const opts = { defaultCollapsed: config.tools[toolName].default_collapsed };
77
+ if (toolName === "moonshot_search") return buildMoonshotSearchTool(opts);
78
+ if (toolName === "moonshot_fetch") return buildMoonshotFetchTool(opts);
79
+ if (toolName === "kimi_datasource") return buildKimiDatasourceTool(opts);
80
+ return undefined;
81
+ }
82
+
74
83
  function registerConfiguredMoonshotTools(
75
84
  pi: ExtensionAPI,
76
85
  config: KimiCodeConfig,
77
86
  options: { updateActiveTools: boolean },
78
87
  ): void {
79
- if (config.tools.moonshot_search.enabled) {
80
- pi.registerTool(
81
- buildMoonshotSearchTool({
82
- defaultCollapsed: config.tools.moonshot_search.default_collapsed,
83
- }),
84
- );
85
- }
86
- if (config.tools.moonshot_fetch.enabled) {
87
- pi.registerTool(
88
- buildMoonshotFetchTool({
89
- defaultCollapsed: config.tools.moonshot_fetch.default_collapsed,
90
- }),
91
- );
88
+ for (const toolName of KIMI_TOOL_NAMES) {
89
+ if (config.tools[toolName].enabled) {
90
+ const tool = buildKimiTool(toolName, config);
91
+ if (tool) pi.registerTool(tool);
92
+ }
92
93
  }
93
94
 
94
95
  if (!options.updateActiveTools) return;
95
96
 
96
97
  const activeTools = new Set(pi.getActiveTools());
97
- for (const toolName of MOONSHOT_TOOL_NAMES) {
98
+ for (const toolName of KIMI_TOOL_NAMES) {
98
99
  if (config.tools[toolName].enabled) {
99
100
  activeTools.add(toolName);
100
101
  } else {
@@ -186,6 +187,17 @@ function quotaBar(remaining: number, limit: number): string {
186
187
  return `[${"#".repeat(filled)}${"-".repeat(width - filled)}]`;
187
188
  }
188
189
 
190
+ function fetchKimiUsage(token: string, signal: AbortSignal): Promise<Response> {
191
+ return fetch("https://api.kimi.com/coding/v1/usages", {
192
+ method: "GET",
193
+ headers: {
194
+ ...getCommonHeaders(),
195
+ Authorization: `Bearer ${token}`,
196
+ },
197
+ signal,
198
+ });
199
+ }
200
+
189
201
  async function fetchKimiUsageSummary(): Promise<string> {
190
202
  const token = getKimiUsageToken();
191
203
  if (!token) return "Usage: missing credentials. Run /login kimi-coding.";
@@ -193,14 +205,11 @@ async function fetchKimiUsageSummary(): Promise<string> {
193
205
  const controller = new AbortController();
194
206
  const timeout = setTimeout(() => controller.abort(), 15_000);
195
207
  try {
196
- const response = await fetch("https://api.kimi.com/coding/v1/usages", {
197
- method: "GET",
198
- headers: {
199
- ...getCommonHeaders(),
200
- Authorization: `Bearer ${token}`,
201
- },
202
- signal: controller.signal,
203
- });
208
+ let response = await fetchKimiUsage(token, controller.signal);
209
+ if (response.status === 401) {
210
+ const refreshed = await refreshKimiAuthToken(token);
211
+ if (refreshed) response = await fetchKimiUsage(refreshed, controller.signal);
212
+ }
204
213
  if (!response.ok) return `Usage: fetch failed (${response.status})`;
205
214
  return parseUsageSummary(await response.json());
206
215
  } catch (error) {
@@ -212,7 +221,7 @@ async function fetchKimiUsageSummary(): Promise<string> {
212
221
  }
213
222
 
214
223
  function moonshotStatus(config: KimiCodeConfig): string {
215
- return MOONSHOT_TOOL_NAMES.map((toolName) => {
224
+ return KIMI_TOOL_NAMES.map((toolName) => {
216
225
  const tool = config.tools[toolName];
217
226
  const enabled = tool.enabled ? "enabled" : "disabled";
218
227
  const collapsed = tool.default_collapsed ? "collapsed" : "expanded";
@@ -220,7 +229,7 @@ function moonshotStatus(config: KimiCodeConfig): string {
220
229
  }).join("\n");
221
230
  }
222
231
 
223
- function toggleEnabled(config: KimiCodeConfig, toolName: MoonshotToolName): KimiCodeConfig {
232
+ function toggleEnabled(config: KimiCodeConfig, toolName: KimiToolName): KimiCodeConfig {
224
233
  return {
225
234
  tools: {
226
235
  ...config.tools,
@@ -232,7 +241,7 @@ function toggleEnabled(config: KimiCodeConfig, toolName: MoonshotToolName): Kimi
232
241
  };
233
242
  }
234
243
 
235
- function toggleCollapsed(config: KimiCodeConfig, toolName: MoonshotToolName): KimiCodeConfig {
244
+ function toggleCollapsed(config: KimiCodeConfig, toolName: KimiToolName): KimiCodeConfig {
236
245
  return {
237
246
  tools: {
238
247
  ...config.tools,
@@ -278,18 +287,15 @@ async function editConfigScope(
278
287
  ): Promise<KimiCodeConfig> {
279
288
  let current = loadScopeKimiCodeConfig(scope, ctx.cwd);
280
289
  while (true) {
281
- const choice = await ctx.ui.select(buildConfigScopeTitle(scope, current, ctx.cwd), [
282
- toolMenuItem(current, "moonshot_search"),
283
- toolMenuItem(current, "moonshot_fetch"),
284
- "Back",
285
- ]);
290
+ const items = KIMI_TOOL_NAMES.map((name) => toolMenuItem(current, name));
291
+ items.push("Back");
292
+ const choice = await ctx.ui.select(buildConfigScopeTitle(scope, current, ctx.cwd), items);
286
293
  if (!choice || choice === "Back") {
287
294
  return loadKimiCodeConfig({ cwd: ctx.cwd, home: os.homedir() });
288
295
  }
289
- if (choice.startsWith("moonshot_search")) {
290
- current = await editMoonshotTool(pi, ctx, scope, current, "moonshot_search");
291
- } else if (choice.startsWith("moonshot_fetch")) {
292
- current = await editMoonshotTool(pi, ctx, scope, current, "moonshot_fetch");
296
+ const toolName = KIMI_TOOL_NAMES.find((name) => choice.startsWith(name));
297
+ if (toolName) {
298
+ current = await editMoonshotTool(pi, ctx, scope, current, toolName);
293
299
  }
294
300
  }
295
301
  }
@@ -299,7 +305,7 @@ async function editMoonshotTool(
299
305
  ctx: ExtensionCommandContext,
300
306
  scope: KimiConfigScope,
301
307
  config: KimiCodeConfig,
302
- toolName: MoonshotToolName,
308
+ toolName: KimiToolName,
303
309
  ): Promise<KimiCodeConfig> {
304
310
  let current = config;
305
311
  while (true) {
@@ -322,6 +328,7 @@ async function editMoonshotTool(
322
328
  const effective = loadKimiCodeConfig({ cwd: ctx.cwd, home: os.homedir() });
323
329
  registerConfiguredMoonshotTools(pi, effective, { updateActiveTools: true });
324
330
  ctx.ui.notify(`Saved ${toolName} config`, "info");
331
+ return current;
325
332
  }
326
333
  }
327
334
 
@@ -342,8 +349,17 @@ function saveScopeKimiCodeConfig(
342
349
  }
343
350
  }
344
351
 
345
- function buildKimiMainTitle(_config: KimiCodeConfig, _cwd: string): string {
346
- return "Kimi settings";
352
+ function buildKimiMainTitle(config: KimiCodeConfig, cwd: string): string {
353
+ const sources = loadKimiCodeConfigSources({ cwd, home: os.homedir() });
354
+ return [
355
+ "Kimi settings",
356
+ "",
357
+ "Effective tools:",
358
+ ...KIMI_TOOL_NAMES.map((toolName) => {
359
+ const enabled = config.tools[toolName].enabled ? "enabled" : "disabled";
360
+ return `- ${toolName}: ${enabled} (${sources.tools[toolName].enabled})`;
361
+ }),
362
+ ].join("\n");
347
363
  }
348
364
 
349
365
  function buildConfigScopeTitle(
@@ -363,11 +379,11 @@ function homeRelative(filePath: string): string {
363
379
  return filePath.startsWith(`${home}/`) ? `~/${filePath.slice(home.length + 1)}` : filePath;
364
380
  }
365
381
 
366
- function toolMenuItem(config: KimiCodeConfig, toolName: MoonshotToolName): string {
382
+ function toolMenuItem(config: KimiCodeConfig, toolName: KimiToolName): string {
367
383
  return `${toolName} -> ${formatToolStatus(config, toolName)}`;
368
384
  }
369
385
 
370
- function formatToolStatus(config: KimiCodeConfig, toolName: MoonshotToolName): string {
386
+ function formatToolStatus(config: KimiCodeConfig, toolName: KimiToolName): string {
371
387
  const tool = config.tools[toolName];
372
388
  const enabled = tool.enabled ? "enabled" : "disabled";
373
389
  const collapsed = tool.default_collapsed ? "default collapsed" : "default expanded";
@@ -379,12 +395,10 @@ export default function (pi: ExtensionAPI) {
379
395
 
380
396
  pi.registerProvider(PROVIDER_ID, {
381
397
  baseUrl: getBaseUrl(),
382
- apiKey: "KIMI_API_KEY",
398
+ apiKey: "$KIMI_API_KEY",
383
399
  api: KIMI_API_TYPE,
384
400
  streamSimple: streamSimpleKimi,
385
401
 
386
- headers: getCommonHeaders(),
387
-
388
402
  models: [
389
403
  applyKimiEnvOverridesToModel({
390
404
  id: "kimi-for-coding",
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "pi-provider-kimi-code",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Use Kimi Code in Pi without losing the Kimi-specific bits",
5
5
  "keywords": [
6
6
  "ai-provider",
7
7
  "coding-agent",
8
- "device-code",
9
8
  "kimi",
10
- "kimi-coder",
9
+ "kimi-code",
11
10
  "kimi-for-coding",
12
11
  "kimi-k2",
13
12
  "kimi-k2.5",
14
13
  "kimi-k2.6",
14
+ "kimi-k2.7",
15
15
  "llm",
16
16
  "llm-provider",
17
17
  "moonshot",
@@ -20,10 +20,8 @@
20
20
  "pi",
21
21
  "pi-coding-agent",
22
22
  "pi-extension",
23
- "pi-mono",
24
23
  "pi-package",
25
- "pi-provider",
26
- "subscription"
24
+ "pi-provider"
27
25
  ],
28
26
  "bugs": {
29
27
  "url": "https://github.com/Leechael/pi-provider-kimi-code/issues"
@@ -54,7 +52,8 @@
54
52
  "devDependencies": {
55
53
  "@earendil-works/pi-ai": "^0.78.0",
56
54
  "@earendil-works/pi-coding-agent": "^0.78.0",
57
- "@j178/prek": "^0.3.8",
55
+ "@earendil-works/pi-tui": "^0.78.1",
56
+ "@j178/prek": "^0.4.4",
58
57
  "@types/node": "^24.0.0",
59
58
  "oxfmt": "^0.44.0",
60
59
  "oxlint": "^1.59.0",
@@ -62,7 +61,8 @@
62
61
  },
63
62
  "peerDependencies": {
64
63
  "@earendil-works/pi-ai": "*",
65
- "@earendil-works/pi-coding-agent": "*"
64
+ "@earendil-works/pi-coding-agent": "*",
65
+ "@earendil-works/pi-tui": "*"
66
66
  },
67
67
  "pi": {
68
68
  "extensions": [
package/src/config.ts CHANGED
@@ -1,11 +1,18 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
 
4
+ export const KIMI_TOOL_NAMES = ["moonshot_search", "moonshot_fetch", "kimi_datasource"] as const;
5
+
6
+ export type KimiToolName = (typeof KIMI_TOOL_NAMES)[number];
7
+
8
+ export type KimiConfigSource = "project" | "home" | "default";
9
+
4
10
  export interface KimiCodeConfig {
5
- tools: {
6
- moonshot_search: { enabled: boolean; default_collapsed: boolean };
7
- moonshot_fetch: { enabled: boolean; default_collapsed: boolean };
8
- };
11
+ tools: Record<KimiToolName, { enabled: boolean; default_collapsed: boolean }>;
12
+ }
13
+
14
+ export interface KimiCodeConfigSources {
15
+ tools: Record<KimiToolName, { enabled: KimiConfigSource; default_collapsed: KimiConfigSource }>;
9
16
  }
10
17
 
11
18
  export interface LoadKimiCodeConfigOptions {
@@ -13,11 +20,16 @@ export interface LoadKimiCodeConfigOptions {
13
20
  home: string;
14
21
  }
15
22
 
23
+ function makeDefaultTools(): KimiCodeConfig["tools"] {
24
+ const tools = {} as KimiCodeConfig["tools"];
25
+ for (const name of KIMI_TOOL_NAMES) {
26
+ tools[name] = { enabled: false, default_collapsed: true };
27
+ }
28
+ return tools;
29
+ }
30
+
16
31
  export const DEFAULT_KIMI_CODE_CONFIG: KimiCodeConfig = {
17
- tools: {
18
- moonshot_search: { enabled: false, default_collapsed: true },
19
- moonshot_fetch: { enabled: false, default_collapsed: true },
20
- },
32
+ tools: makeDefaultTools(),
21
33
  };
22
34
 
23
35
  export function getGlobalKimiCodeConfigPath(home: string): string {
@@ -85,23 +97,61 @@ function readDefaultCollapsed(config: Record<string, unknown>, toolName: string)
85
97
  return tool.default_collapsed !== false;
86
98
  }
87
99
 
100
+ function hasToolField(
101
+ config: Record<string, unknown>,
102
+ toolName: KimiToolName,
103
+ field: "enabled" | "default_collapsed",
104
+ ): boolean {
105
+ const tools = config.tools;
106
+ if (!isRecord(tools)) return false;
107
+ const tool = tools[toolName];
108
+ if (!isRecord(tool)) return false;
109
+ return Object.hasOwn(tool, field);
110
+ }
111
+
112
+ function readToolConfig(
113
+ config: Record<string, unknown>,
114
+ toolName: KimiToolName,
115
+ ): { enabled: boolean; default_collapsed: boolean } {
116
+ return {
117
+ enabled: readEnabled(config, toolName),
118
+ default_collapsed: readDefaultCollapsed(config, toolName),
119
+ };
120
+ }
121
+
88
122
  export function loadKimiCodeConfig(options: LoadKimiCodeConfigOptions): KimiCodeConfig {
89
123
  const globalConfig = readConfigFile(getGlobalKimiCodeConfigPath(options.home));
90
124
  const projectConfig = readConfigFile(getProjectKimiCodeConfigPath(options.cwd));
91
125
  const merged = mergeConfig(globalConfig, projectConfig);
92
126
 
93
- return {
94
- tools: {
95
- moonshot_search: {
96
- enabled: readEnabled(merged, "moonshot_search"),
97
- default_collapsed: readDefaultCollapsed(merged, "moonshot_search"),
98
- },
99
- moonshot_fetch: {
100
- enabled: readEnabled(merged, "moonshot_fetch"),
101
- default_collapsed: readDefaultCollapsed(merged, "moonshot_fetch"),
102
- },
103
- },
104
- };
127
+ const tools = {} as KimiCodeConfig["tools"];
128
+ for (const name of KIMI_TOOL_NAMES) {
129
+ tools[name] = readToolConfig(merged, name);
130
+ }
131
+ return { tools };
132
+ }
133
+
134
+ export function loadKimiCodeConfigSources(
135
+ options: LoadKimiCodeConfigOptions,
136
+ ): KimiCodeConfigSources {
137
+ const homeConfig = readConfigFileQuiet(getGlobalKimiCodeConfigPath(options.home));
138
+ const projectConfig = readConfigFileQuiet(getProjectKimiCodeConfigPath(options.cwd));
139
+ const tools = {} as KimiCodeConfigSources["tools"];
140
+ for (const name of KIMI_TOOL_NAMES) {
141
+ tools[name] = {
142
+ enabled: hasToolField(projectConfig, name, "enabled")
143
+ ? "project"
144
+ : hasToolField(homeConfig, name, "enabled")
145
+ ? "home"
146
+ : "default",
147
+ default_collapsed: hasToolField(projectConfig, name, "default_collapsed")
148
+ ? "project"
149
+ : hasToolField(homeConfig, name, "default_collapsed")
150
+ ? "home"
151
+ : "default",
152
+ };
153
+ }
154
+ return { tools };
105
155
  }
106
156
 
107
157
  export function loadProjectKimiCodeConfig(cwd: string): KimiCodeConfig {
@@ -114,18 +164,11 @@ export function loadHomeKimiCodeConfig(home: string): KimiCodeConfig {
114
164
 
115
165
  function loadKimiCodeConfigFile(path: string): KimiCodeConfig {
116
166
  const projectConfig = readConfigFileQuiet(path);
117
- return {
118
- tools: {
119
- moonshot_search: {
120
- enabled: readEnabled(projectConfig, "moonshot_search"),
121
- default_collapsed: readDefaultCollapsed(projectConfig, "moonshot_search"),
122
- },
123
- moonshot_fetch: {
124
- enabled: readEnabled(projectConfig, "moonshot_fetch"),
125
- default_collapsed: readDefaultCollapsed(projectConfig, "moonshot_fetch"),
126
- },
127
- },
128
- };
167
+ const tools = {} as KimiCodeConfig["tools"];
168
+ for (const name of KIMI_TOOL_NAMES) {
169
+ tools[name] = readToolConfig(projectConfig, name);
170
+ }
171
+ return { tools };
129
172
  }
130
173
 
131
174
  export function saveProjectKimiCodeConfig(cwd: string, config: KimiCodeConfig): void {
@@ -138,13 +181,15 @@ export function saveHomeKimiCodeConfig(home: string, config: KimiCodeConfig): vo
138
181
 
139
182
  function saveKimiCodeConfigFile(path: string, config: KimiCodeConfig): void {
140
183
  const raw = readConfigFileQuiet(path);
184
+ const tools: Record<string, unknown> = {
185
+ ...(isRecord(raw.tools) ? raw.tools : {}),
186
+ };
187
+ for (const name of KIMI_TOOL_NAMES) {
188
+ tools[name] = { ...config.tools[name] };
189
+ }
141
190
  const next = {
142
191
  ...raw,
143
- tools: {
144
- ...(isRecord(raw.tools) ? raw.tools : {}),
145
- moonshot_search: { ...config.tools.moonshot_search },
146
- moonshot_fetch: { ...config.tools.moonshot_fetch },
147
- },
192
+ tools,
148
193
  };
149
194
  mkdirSync(dirname(path), { recursive: true });
150
195
  writeFileSync(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
package/src/models.ts CHANGED
@@ -13,6 +13,7 @@ export interface KimiOAuthExtras {
13
13
  contextLength?: number;
14
14
  supportsReasoning?: boolean;
15
15
  supportsImageIn?: boolean;
16
+ supportsThinkingType?: "only" | "no" | "both";
16
17
  }
17
18
 
18
19
  export type KimiOAuthCredentials = OAuthCredentials & KimiOAuthExtras;
@@ -23,6 +24,12 @@ interface KimiServerModel {
23
24
  context_length?: unknown;
24
25
  supports_reasoning?: unknown;
25
26
  supports_image_in?: unknown;
27
+ supports_thinking_type?: unknown;
28
+ }
29
+
30
+ function parseSupportsThinkingType(value: unknown): "only" | "no" | "both" | undefined {
31
+ if (value === "only" || value === "no" || value === "both") return value;
32
+ return undefined;
26
33
  }
27
34
 
28
35
  export function buildModelsUrl(baseUrl: string): string {
@@ -55,7 +62,10 @@ export async function discoverKimiModelMetadata(accessToken: string): Promise<Ki
55
62
  if (typeof preferred.context_length === "number" && preferred.context_length > 0) {
56
63
  extras.contextLength = preferred.context_length;
57
64
  }
58
- if (typeof preferred.supports_reasoning === "boolean") {
65
+ const thinkingType = parseSupportsThinkingType(preferred.supports_thinking_type);
66
+ if (thinkingType) {
67
+ extras.supportsThinkingType = thinkingType;
68
+ } else if (typeof preferred.supports_reasoning === "boolean") {
59
69
  extras.supportsReasoning = preferred.supports_reasoning;
60
70
  }
61
71
  if (typeof preferred.supports_image_in === "boolean") {
@@ -71,7 +81,8 @@ export function applyKimiOAuthExtrasToModel(
71
81
  model: Model<Api>,
72
82
  extras: KimiOAuthExtras,
73
83
  ): Model<Api> {
74
- const next: Model<Api> & { wireModelId?: string } = { ...model };
84
+ const next: Model<Api> & { wireModelId?: string; supportsThinkingType?: "only" | "no" | "both" } =
85
+ { ...model };
75
86
  if (typeof extras.modelDisplay === "string" && extras.modelDisplay) {
76
87
  next.name = extras.modelDisplay;
77
88
  }
@@ -81,8 +92,12 @@ export function applyKimiOAuthExtrasToModel(
81
92
  if (typeof extras.wireModelId === "string" && extras.wireModelId) {
82
93
  next.wireModelId = extras.wireModelId;
83
94
  }
84
- if (typeof extras.supportsReasoning === "boolean") {
95
+ if (typeof extras.supportsThinkingType === "string") {
96
+ next.reasoning = extras.supportsThinkingType !== "no";
97
+ next.supportsThinkingType = extras.supportsThinkingType;
98
+ } else if (typeof extras.supportsReasoning === "boolean") {
85
99
  next.reasoning = extras.supportsReasoning;
100
+ next.supportsThinkingType = undefined;
86
101
  }
87
102
  if (typeof extras.supportsImageIn === "boolean") {
88
103
  const input = ["text"];
@@ -101,7 +116,11 @@ function parseKimiModelCapabilities(value: string | undefined): KimiOAuthExtras
101
116
  .filter(Boolean),
102
117
  );
103
118
  return {
104
- supportsReasoning: caps.has("thinking") || caps.has("always_thinking"),
119
+ supportsThinkingType: caps.has("always_thinking")
120
+ ? "only"
121
+ : caps.has("thinking")
122
+ ? "both"
123
+ : "no",
105
124
  supportsImageIn: caps.has("image_in"),
106
125
  };
107
126
  }
package/src/payload.ts CHANGED
@@ -39,6 +39,7 @@ export interface KimiPayloadContext {
39
39
  reasoning?: ThinkingLevel;
40
40
  thinkingKeep?: string;
41
41
  envOverrides: KimiEnvOverrides;
42
+ supportsThinkingType?: "only" | "no" | "both";
42
43
  }
43
44
 
44
45
  export function isRecord(value: unknown): value is JsonRecord {
@@ -54,6 +55,16 @@ function mapThinkingLevel(level?: string): { effort: string | null; enabled: boo
54
55
  return undefined;
55
56
  }
56
57
 
58
+ function resolveThinkingLevel(ctx: KimiPayloadContext): ThinkingLevel | undefined {
59
+ if (ctx.supportsThinkingType === "no") return undefined;
60
+ if (ctx.supportsThinkingType === "only") {
61
+ if (!ctx.reasoning) return "low";
62
+ const mapped = mapThinkingLevel(ctx.reasoning);
63
+ if (mapped && !mapped.enabled) return "low";
64
+ }
65
+ return ctx.reasoning;
66
+ }
67
+
57
68
  function parseInlineUploadThreshold(raw: string | undefined): number {
58
69
  const parsed = Number.parseInt(raw ?? "", 10);
59
70
  return Number.isFinite(parsed) && parsed >= 0
@@ -435,8 +446,9 @@ export async function applyKimiPayloadMutations(
435
446
  }
436
447
 
437
448
  // 5. Reasoning effort mapping.
438
- if (ctx.reasoning) {
439
- const mapped = mapThinkingLevel(ctx.reasoning);
449
+ const resolvedReasoning = resolveThinkingLevel(ctx);
450
+ if (resolvedReasoning) {
451
+ const mapped = mapThinkingLevel(resolvedReasoning);
440
452
  if (mapped) {
441
453
  payload.reasoning_effort = mapped.effort;
442
454
  const extraBody = isRecord(payload.extra_body) ? payload.extra_body : {};
package/src/stream.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  } from "@earendil-works/pi-ai";
20
20
 
21
21
  import { IS_OPENAI_PROTOCOL, PROTOCOL } from "./constants.ts";
22
+ import { getCommonHeaders } from "./device.ts";
22
23
  import { isKimiAuthErrorMessage, refreshKimiAuthToken } from "./oauth.ts";
23
24
  import {
24
25
  type Uploader,
@@ -136,13 +137,26 @@ export async function* filterEmptyResponseStream(
136
137
  // Stream wrapper: orchestrates payload mutation + event filter
137
138
  // =============================================================================
138
139
 
140
+ const KIMI_API_KEY_ENV_REFERENCES = new Set(["$KIMI_API_KEY", "${KIMI_API_KEY}"]);
141
+
142
+ export function resolveKimiApiKey(apiKey: string | undefined): string {
143
+ if (apiKey !== undefined && KIMI_API_KEY_ENV_REFERENCES.has(apiKey)) {
144
+ return process.env.KIMI_API_KEY?.trim() || "";
145
+ }
146
+ return apiKey || process.env.KIMI_API_KEY || "";
147
+ }
148
+
149
+ export function mergeKimiRequestHeaders(headers?: Record<string, string>): Record<string, string> {
150
+ return { ...getCommonHeaders(), ...headers };
151
+ }
152
+
139
153
  export function streamSimpleKimi(
140
154
  model: Model<Api>,
141
155
  context: Context,
142
156
  options?: SimpleStreamOptions,
143
157
  ): AssistantMessageEventStream {
144
158
  const filtered = createAssistantMessageEventStream();
145
- const initialKey = options?.apiKey || process.env.KIMI_API_KEY || "";
159
+ const initialKey = resolveKimiApiKey(options?.apiKey);
146
160
 
147
161
  const cacheKeyOverride = (
148
162
  options as (SimpleStreamOptions & { prompt_cache_key?: unknown }) | undefined
@@ -158,6 +172,9 @@ export function streamSimpleKimi(
158
172
  // the model object via modifyModels and rewritten into the request payload
159
173
  // here so /v1/chat/completions and /v1/messages see the real wire id.
160
174
  const wireModelId = (model as Model<Api> & { wireModelId?: unknown }).wireModelId;
175
+ const supportsThinkingType = (
176
+ model as Model<Api> & { supportsThinkingType?: "only" | "no" | "both" }
177
+ ).supportsThinkingType;
161
178
 
162
179
  const buildPatchedOptions = (apiKey: string): SimpleStreamOptions => {
163
180
  const upload: Uploader | undefined = apiKey
@@ -169,6 +186,7 @@ export function streamSimpleKimi(
169
186
  return {
170
187
  ...options,
171
188
  ...apiKeyOverride,
189
+ headers: mergeKimiRequestHeaders(options?.headers),
172
190
  onPayload: async (payload, modelData) => {
173
191
  let nextPayload: unknown = payload;
174
192
 
@@ -181,6 +199,7 @@ export function streamSimpleKimi(
181
199
  reasoning: options?.reasoning,
182
200
  thinkingKeep,
183
201
  envOverrides,
202
+ supportsThinkingType,
184
203
  });
185
204
  if (
186
205
  typeof wireModelId === "string" &&
@@ -0,0 +1,115 @@
1
+ import { AuthStorage, type AgentToolResult } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth } from "@earendil-works/pi-tui";
3
+
4
+ import { PROVIDER_ID, getBaseUrl } from "../constants.ts";
5
+ import { getCommonHeaders } from "../device.ts";
6
+ import { refreshKimiAuthToken } from "../oauth.ts";
7
+
8
+ export const KIMI_TOOL_TIMEOUT_MS = 180_000;
9
+
10
+ export interface KimiToolDeps {
11
+ fetch: typeof fetch;
12
+ getAccessToken: () => string | null;
13
+ refreshAccessToken: (currentToken: string) => Promise<string | null>;
14
+ }
15
+
16
+ export interface BuildKimiToolOptions {
17
+ deps?: Partial<KimiToolDeps>;
18
+ defaultCollapsed?: boolean;
19
+ }
20
+
21
+ function defaultGetAccessToken(): string | null {
22
+ const credential = AuthStorage.create().get(PROVIDER_ID);
23
+ if (!credential || credential.type !== "oauth") return null;
24
+ return credential.access;
25
+ }
26
+
27
+ export function buildKimiToolDeps(options: BuildKimiToolOptions = {}): KimiToolDeps {
28
+ return {
29
+ fetch: options.deps?.fetch ?? fetch,
30
+ getAccessToken: options.deps?.getAccessToken ?? defaultGetAccessToken,
31
+ refreshAccessToken: options.deps?.refreshAccessToken ?? refreshKimiAuthToken,
32
+ };
33
+ }
34
+
35
+ export function buildTimeoutSignal(signal: AbortSignal | undefined): {
36
+ signal: AbortSignal;
37
+ cleanup: () => void;
38
+ } {
39
+ const controller = new AbortController();
40
+ const timeout = setTimeout(() => controller.abort(), KIMI_TOOL_TIMEOUT_MS);
41
+ const abort = () => controller.abort();
42
+ if (signal?.aborted) {
43
+ controller.abort();
44
+ } else {
45
+ signal?.addEventListener("abort", abort, { once: true });
46
+ }
47
+
48
+ return {
49
+ signal: controller.signal,
50
+ cleanup: () => {
51
+ clearTimeout(timeout);
52
+ signal?.removeEventListener("abort", abort);
53
+ },
54
+ };
55
+ }
56
+
57
+ export function getKimiBaseV1(): string {
58
+ const base = getBaseUrl().replace(/\/+$/, "");
59
+ return base.endsWith("/v1") ? base : `${base}/v1`;
60
+ }
61
+
62
+ export function getKimiDatasourceUrl(): string {
63
+ const explicit = process.env.KIMI_DATASOURCE_API_URL?.trim();
64
+ if (explicit) return explicit;
65
+ return `${getKimiBaseV1()}/tools`;
66
+ }
67
+
68
+ export function buildHeaders(accessToken: string, toolCallId: string): Record<string, string> {
69
+ return {
70
+ ...getCommonHeaders(),
71
+ Authorization: `Bearer ${accessToken}`,
72
+ "Content-Type": "application/json",
73
+ "X-Msh-Tool-Call-Id": toolCallId,
74
+ };
75
+ }
76
+
77
+ export function errorResult<T>(message: string): AgentToolResult<T> {
78
+ return {
79
+ content: [{ type: "text", text: message }],
80
+ details: undefined as T,
81
+ };
82
+ }
83
+
84
+ export async function readErrorBody(response: Response): Promise<string> {
85
+ return response.text().catch(() => "");
86
+ }
87
+
88
+ export async function fetchWithAuthRetry(
89
+ deps: KimiToolDeps,
90
+ accessToken: string,
91
+ request: (token: string) => Promise<Response>,
92
+ ): Promise<Response> {
93
+ const response = await request(accessToken);
94
+ if (response.status !== 401) return response;
95
+
96
+ const refreshed = await deps.refreshAccessToken(accessToken);
97
+ if (!refreshed || refreshed === accessToken) return response;
98
+ return request(refreshed);
99
+ }
100
+
101
+ export function textComponent(text: string) {
102
+ return {
103
+ render: (width: number) => text.split("\n").map((line) => truncateToWidth(line, width, "…")),
104
+ invalidate: () => {},
105
+ };
106
+ }
107
+
108
+ export function firstText(result: AgentToolResult<unknown>): string {
109
+ const first = result.content[0];
110
+ return first?.type === "text" ? first.text : "";
111
+ }
112
+
113
+ export function shouldCollapse(defaultCollapsed: boolean, expanded: boolean | undefined): boolean {
114
+ return defaultCollapsed && expanded !== true;
115
+ }
@@ -0,0 +1,191 @@
1
+ import { defineTool } from "@earendil-works/pi-coding-agent";
2
+ import { Type, type Static } from "@earendil-works/pi-ai";
3
+
4
+ import {
5
+ type BuildKimiToolOptions,
6
+ buildHeaders,
7
+ getKimiDatasourceUrl,
8
+ buildKimiToolDeps,
9
+ buildTimeoutSignal,
10
+ errorResult,
11
+ fetchWithAuthRetry,
12
+ firstText,
13
+ readErrorBody,
14
+ shouldCollapse,
15
+ textComponent,
16
+ } from "./common.ts";
17
+
18
+ const DATASOURCE_DEFINITIONS = [
19
+ {
20
+ name: "stock_finance_data",
21
+ description: "A/HK/US stocks, prices, financial reports, indicators, shareholders.",
22
+ },
23
+ {
24
+ name: "yahoo_finance",
25
+ description: "Global finance: quotes, analyst ratings, options, indexes, historical prices.",
26
+ },
27
+ {
28
+ name: "world_bank_open_data",
29
+ description: "World Bank macro data: GDP, inflation, population, country comparisons.",
30
+ },
31
+ {
32
+ name: "tianyancha",
33
+ description: "Chinese enterprise registry: shareholders, legal representative, risks, patents.",
34
+ },
35
+ { name: "arxiv", description: "arXiv preprints: paper search, abstracts, arXiv IDs." },
36
+ {
37
+ name: "scholar",
38
+ description: "Google Scholar: academic papers, authors, citations, high-citation surveys.",
39
+ },
40
+ {
41
+ name: "yuandian_law",
42
+ description: "Chinese laws, regulations, judicial cases; labor contracts and legal clauses.",
43
+ },
44
+ ] as const;
45
+
46
+ const DATASOURCE_NAMES = DATASOURCE_DEFINITIONS.map((source) => source.name);
47
+
48
+ export const kimiDatasourceSchema = Type.Object({
49
+ data_source_name: Type.Union(
50
+ DATASOURCE_NAMES.map((name) => Type.Literal(name)),
51
+ {
52
+ description:
53
+ "Source: stock_finance_data stocks/reports; yahoo_finance quotes/options; world_bank_open_data GDP/population; tianyancha Chinese company registry/legal rep; arxiv preprints; scholar citations; yuandian_law Chinese laws/cases/contracts.",
54
+ },
55
+ ),
56
+ api_name: Type.Optional(
57
+ Type.String({
58
+ description: "API name from the datasource docs. Omit to get docs first.",
59
+ }),
60
+ ),
61
+ params: Type.Optional(
62
+ Type.Record(Type.String(), Type.Unknown(), {
63
+ description: "Parameters matching the selected datasource API docs.",
64
+ }),
65
+ ),
66
+ });
67
+
68
+ export type KimiDatasourceParams = Static<typeof kimiDatasourceSchema>;
69
+
70
+ interface DatasourceToolResponse {
71
+ is_success?: boolean;
72
+ error?: unknown;
73
+ result?: {
74
+ assistant?: unknown;
75
+ user?: unknown;
76
+ };
77
+ }
78
+
79
+ function extractTextFromResult(result: unknown): string {
80
+ if (!Array.isArray(result)) return "";
81
+ const texts: string[] = [];
82
+ for (const item of result) {
83
+ if (typeof item === "string") {
84
+ texts.push(item);
85
+ } else if (typeof item === "object" && item !== null) {
86
+ const record = item as Record<string, unknown>;
87
+ if (typeof record.text === "string") {
88
+ texts.push(record.text);
89
+ }
90
+ }
91
+ }
92
+ return texts.join("\n");
93
+ }
94
+
95
+ function extractResponseText(data: DatasourceToolResponse): string {
96
+ if (data.result?.assistant) {
97
+ const text = extractTextFromResult(data.result.assistant);
98
+ if (text) return text;
99
+ }
100
+ if (data.result?.user) {
101
+ const text = extractTextFromResult(data.result.user);
102
+ if (text) return text;
103
+ }
104
+ return JSON.stringify(data, null, 2);
105
+ }
106
+
107
+ export function buildKimiDatasourceTool(options: BuildKimiToolOptions = {}) {
108
+ const deps = buildKimiToolDeps(options);
109
+ const defaultCollapsed = options.defaultCollapsed ?? true;
110
+
111
+ return defineTool({
112
+ name: "kimi_datasource",
113
+ label: "Kimi Datasource",
114
+ description:
115
+ "Call Kimi datasource APIs. Covers stocks/finance, macro data, Chinese company registry, papers, and Chinese law/cases/contracts. First omit api_name to get docs, then call api_name with documented params.",
116
+ promptSnippet: "Query Kimi datasource APIs; get docs first, then call documented api_name",
117
+ parameters: kimiDatasourceSchema,
118
+
119
+ async execute(toolCallId, params, signal) {
120
+ const accessToken = deps.getAccessToken();
121
+ if (!accessToken) {
122
+ return errorResult<string>(
123
+ "Missing Kimi Code OAuth credentials. Run /login kimi-coding first.",
124
+ );
125
+ }
126
+
127
+ const timeout = buildTimeoutSignal(signal);
128
+ try {
129
+ const apiName = params.api_name?.trim();
130
+ const body = apiName
131
+ ? {
132
+ method: "call_data_source_tool",
133
+ params: {
134
+ data_source_name: params.data_source_name,
135
+ api_name: apiName,
136
+ params: params.params ?? {},
137
+ },
138
+ }
139
+ : {
140
+ method: "get_data_source_desc",
141
+ params: { data_source_name: params.data_source_name },
142
+ };
143
+
144
+ const response = await fetchWithAuthRetry(deps, accessToken, (token) =>
145
+ deps.fetch(getKimiDatasourceUrl(), {
146
+ method: "POST",
147
+ headers: buildHeaders(token, toolCallId),
148
+ body: JSON.stringify(body),
149
+ signal: timeout.signal,
150
+ }),
151
+ );
152
+
153
+ if (!response.ok) {
154
+ const bodyText = await readErrorBody(response);
155
+ return errorResult<string>(
156
+ `kimi_datasource failed: ${response.status}${bodyText ? ` ${bodyText}` : ""}`,
157
+ );
158
+ }
159
+
160
+ const data = (await response.json()) as DatasourceToolResponse;
161
+ if (data.is_success === false) {
162
+ const errorText =
163
+ typeof data.error === "string"
164
+ ? data.error
165
+ : JSON.stringify(data.error ?? data, null, 2);
166
+ return errorResult<string>(`kimi_datasource error: ${errorText}`);
167
+ }
168
+
169
+ const text = extractResponseText(data);
170
+ return {
171
+ content: [{ type: "text", text }],
172
+ details: text,
173
+ };
174
+ } catch (error) {
175
+ const message = error instanceof Error ? error.message : String(error);
176
+ return errorResult<string>(`kimi_datasource failed: ${message}`);
177
+ } finally {
178
+ timeout.cleanup();
179
+ }
180
+ },
181
+
182
+ renderResult(result, renderOptions) {
183
+ const text = firstText(result);
184
+ if (!shouldCollapse(defaultCollapsed, renderOptions.expanded)) {
185
+ return textComponent(text);
186
+ }
187
+ const preview = text.length > 200 ? `${text.slice(0, 200)}…` : text;
188
+ return textComponent(`kimi_datasource: ${text.length} chars\n${preview}`);
189
+ },
190
+ });
191
+ }
@@ -1,12 +1,19 @@
1
- import { AuthStorage, defineTool, type AgentToolResult } from "@earendil-works/pi-coding-agent";
1
+ import { defineTool, type AgentToolResult } from "@earendil-works/pi-coding-agent";
2
2
  import { Type, type Static } from "@earendil-works/pi-ai";
3
3
 
4
- import { PROVIDER_ID } from "../constants.ts";
5
- import { getCommonHeaders } from "../device.ts";
6
- import { refreshKimiAuthToken } from "../oauth.ts";
7
-
8
- const MOONSHOT_BASE_V1 = "https://api.kimi.com/coding/v1";
9
- const MOONSHOT_TIMEOUT_MS = 180_000;
4
+ import {
5
+ type BuildKimiToolOptions,
6
+ buildHeaders,
7
+ getKimiBaseV1,
8
+ buildKimiToolDeps,
9
+ buildTimeoutSignal,
10
+ errorResult,
11
+ fetchWithAuthRetry,
12
+ firstText,
13
+ readErrorBody,
14
+ shouldCollapse,
15
+ textComponent,
16
+ } from "./common.ts";
10
17
 
11
18
  export const moonshotSearchSchema = Type.Object({
12
19
  query: Type.String({ description: "The query text to search for." }),
@@ -45,76 +52,13 @@ export interface MoonshotFetchResult {
45
52
  content: string;
46
53
  }
47
54
 
48
- interface MoonshotToolDeps {
49
- fetch: typeof fetch;
50
- getAccessToken: () => string | null;
51
- refreshAccessToken: (currentToken: string) => Promise<string | null>;
52
- }
53
-
54
- export interface BuildMoonshotToolOptions {
55
- deps?: Partial<MoonshotToolDeps>;
56
- defaultCollapsed?: boolean;
57
- }
58
-
59
- interface MoonshotSearchResponse {
60
- search_results?: unknown;
61
- }
62
-
63
- function defaultGetAccessToken(): string | null {
64
- const credential = AuthStorage.create().get(PROVIDER_ID);
65
- if (!credential || credential.type !== "oauth") return null;
66
- return credential.access;
67
- }
68
-
69
- function buildDeps(options: BuildMoonshotToolOptions = {}): MoonshotToolDeps {
70
- return {
71
- fetch: options.deps?.fetch ?? fetch,
72
- getAccessToken: options.deps?.getAccessToken ?? defaultGetAccessToken,
73
- refreshAccessToken: options.deps?.refreshAccessToken ?? refreshKimiAuthToken,
74
- };
75
- }
76
-
77
55
  function clampLimit(value: number | undefined): number {
78
56
  if (typeof value !== "number" || !Number.isFinite(value)) return 5;
79
57
  return Math.max(1, Math.min(20, Math.trunc(value)));
80
58
  }
81
59
 
82
- function errorResult<T>(message: string): AgentToolResult<T> {
83
- return {
84
- content: [{ type: "text", text: message }],
85
- details: undefined as T,
86
- };
87
- }
88
-
89
- function buildTimeoutSignal(signal: AbortSignal | undefined): {
90
- signal: AbortSignal;
91
- cleanup: () => void;
92
- } {
93
- const controller = new AbortController();
94
- const timeout = setTimeout(() => controller.abort(), MOONSHOT_TIMEOUT_MS);
95
- const abort = () => controller.abort();
96
- if (signal?.aborted) {
97
- controller.abort();
98
- } else {
99
- signal?.addEventListener("abort", abort, { once: true });
100
- }
101
-
102
- return {
103
- signal: controller.signal,
104
- cleanup: () => {
105
- clearTimeout(timeout);
106
- signal?.removeEventListener("abort", abort);
107
- },
108
- };
109
- }
110
-
111
- function buildHeaders(accessToken: string, toolCallId: string): Record<string, string> {
112
- return {
113
- ...getCommonHeaders(),
114
- Authorization: `Bearer ${accessToken}`,
115
- "Content-Type": "application/json",
116
- "X-Msh-Tool-Call-Id": toolCallId,
117
- };
60
+ interface MoonshotSearchResponse {
61
+ search_results?: unknown;
118
62
  }
119
63
 
120
64
  function stringField(record: Record<string, unknown>, key: string): string {
@@ -144,22 +88,6 @@ function searchResultsText(results: MoonshotSearchResult[]): string {
144
88
  return JSON.stringify(results, null, 2);
145
89
  }
146
90
 
147
- function textComponent(text: string) {
148
- return {
149
- render: () => text.split("\n"),
150
- invalidate: () => {},
151
- };
152
- }
153
-
154
- function firstText(result: AgentToolResult<unknown>): string {
155
- const first = result.content[0];
156
- return first?.type === "text" ? first.text : "";
157
- }
158
-
159
- function shouldCollapse(defaultCollapsed: boolean, expanded: boolean | undefined): boolean {
160
- return defaultCollapsed && expanded !== true;
161
- }
162
-
163
91
  function renderSearchResult(
164
92
  result: AgentToolResult<MoonshotSearchResult[]>,
165
93
  expanded: boolean | undefined,
@@ -195,25 +123,8 @@ function renderFetchResult(
195
123
  );
196
124
  }
197
125
 
198
- async function readErrorBody(response: Response): Promise<string> {
199
- return response.text().catch(() => "");
200
- }
201
-
202
- async function fetchWithAuthRetry(
203
- deps: MoonshotToolDeps,
204
- accessToken: string,
205
- request: (token: string) => Promise<Response>,
206
- ): Promise<Response> {
207
- const response = await request(accessToken);
208
- if (response.status !== 401) return response;
209
-
210
- const refreshed = await deps.refreshAccessToken(accessToken);
211
- if (!refreshed || refreshed === accessToken) return response;
212
- return request(refreshed);
213
- }
214
-
215
- export function buildMoonshotSearchTool(options: BuildMoonshotToolOptions = {}) {
216
- const deps = buildDeps(options);
126
+ export function buildMoonshotSearchTool(options: BuildKimiToolOptions = {}) {
127
+ const deps = buildKimiToolDeps(options);
217
128
  const defaultCollapsed = options.defaultCollapsed ?? true;
218
129
 
219
130
  return defineTool({
@@ -236,7 +147,7 @@ export function buildMoonshotSearchTool(options: BuildMoonshotToolOptions = {})
236
147
  const timeout = buildTimeoutSignal(signal);
237
148
  try {
238
149
  const response = await fetchWithAuthRetry(deps, accessToken, (token) =>
239
- deps.fetch(`${MOONSHOT_BASE_V1}/search`, {
150
+ deps.fetch(`${getKimiBaseV1()}/search`, {
240
151
  method: "POST",
241
152
  headers: buildHeaders(token, toolCallId),
242
153
  body: JSON.stringify({
@@ -276,8 +187,8 @@ export function buildMoonshotSearchTool(options: BuildMoonshotToolOptions = {})
276
187
  });
277
188
  }
278
189
 
279
- export function buildMoonshotFetchTool(options: BuildMoonshotToolOptions = {}) {
280
- const deps = buildDeps(options);
190
+ export function buildMoonshotFetchTool(options: BuildKimiToolOptions = {}) {
191
+ const deps = buildKimiToolDeps(options);
281
192
  const defaultCollapsed = options.defaultCollapsed ?? true;
282
193
 
283
194
  return defineTool({
@@ -298,7 +209,7 @@ export function buildMoonshotFetchTool(options: BuildMoonshotToolOptions = {}) {
298
209
  const timeout = buildTimeoutSignal(signal);
299
210
  try {
300
211
  const response = await fetchWithAuthRetry(deps, accessToken, (token) =>
301
- deps.fetch(`${MOONSHOT_BASE_V1}/fetch`, {
212
+ deps.fetch(`${getKimiBaseV1()}/fetch`, {
302
213
  method: "POST",
303
214
  headers: {
304
215
  ...buildHeaders(token, toolCallId),