pi-provider-kimi-code 0.6.0 → 0.6.2

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,9 +5,9 @@
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/datasource 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, tool schema compatibility, live model metadata, measured cache behavior, and API-tested parameter handling.
9
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.
10
+ > **Kimi K2.7 supported.** The provider automatically discovers the current model from Kimi's `/v1/models` endpoint at login, refresh, and `/kimi-settings` time, so new rollouts (including K2.7) are picked up immediately without a package update.
11
11
 
12
12
  ## Why this exists
13
13
 
@@ -18,19 +18,24 @@ Kimi's API surface goes beyond the LLM itself. It includes file uploads, web sea
18
18
  - **Use your Kimi account.** Log in with `/login kimi-coding`, or reuse an existing `kimi-code` session.
19
19
  - **Send files the Kimi way.** Large inline images go through Kimi's Files API and become `ms://` references instead of huge base64 payloads.
20
20
  - **Know what the cache is doing.** Kimi caches by content prefix. This repo measures that behavior instead of pretending `prompt_cache_key` controls it.
21
- - **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.
21
+ - **Keep Pi's tools working.** Moonshot's API rejects tool schemas over 15 KB — a limit that Pi's extension ecosystem regularly hits ([#16](https://github.com/Leechael/pi-provider-kimi-code/issues/16), [#21](https://github.com/Leechael/pi-provider-kimi-code/issues/21)). This extension automatically deduplicates schemas with `$ref`/`$defs` before sending, so subagents and other extensions don't break.
22
+ - **Tested against the live API.** Thinking config, parameter constraints, protocol compatibility, and streaming behavior are all verified against Kimi's Coding endpoint — not assumed from docs. When the API rejects something (wrong `temperature`, unsupported `tool_choice`), the provider strips it before you see a 400.
22
23
  - **Turn on Kimi-native tools when you want them.** `moonshot_search`, `moonshot_fetch`, and `kimi_datasource` are opt-in, configurable per user or per project.
24
+ - **Embed in your own build.** `KimiCode()` factory lets you ship Kimi Code support inside a custom Pi agent with programmatic config overrides — no file-based extension path needed.
23
25
 
24
26
  ## What this package adds
25
27
 
26
28
  - Kimi account login in Pi, plus `KIMI_API_KEY` for CI or pay-per-token use.
27
29
  - `kimi-code` credential reuse from `~/.kimi/credentials/kimi-code.json`.
28
30
  - Kimi Files API uploads for large inline images.
29
- - Live model metadata from Kimi at login / refresh time.
31
+ - Live model metadata from Kimi at login, refresh, and `/kimi-settings` time.
30
32
  - OpenAI-compatible mode by default, Anthropic-compatible mode on request.
31
- - Kimi K2.6 reasoning level mapping for Pi's reasoning controls.
33
+ - Kimi K2.7 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: strips `temperature`, `top_p`, and `tool_choice` values that the API rejects.
32
36
  - Stream cleanup for Kimi's thinking-only placeholder text.
33
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.
34
39
  - No build step; Pi loads the TypeScript extension directly.
35
40
 
36
41
  ## Install
@@ -56,6 +61,21 @@ curl -L https://github.com/Leechael/pi-provider-kimi-code/releases/latest/downlo
56
61
  pi install /tmp/pi-provider-kimi-code
57
62
  ```
58
63
 
64
+ ### Programmatic usage
65
+
66
+ Use `KimiCode()` to embed Kimi Code as a provider inside a custom Pi build:
67
+
68
+ ```typescript
69
+ import { main } from "@earendil-works/pi-coding-agent";
70
+ import { KimiCode } from "pi-provider-kimi-code";
71
+
72
+ main(process.argv.slice(2), {
73
+ extensionFactories: [KimiCode({ protocol: "anthropic" })],
74
+ });
75
+ ```
76
+
77
+ `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
+
59
79
  ## Sign in
60
80
 
61
81
  Inside Pi, run:
@@ -103,7 +123,7 @@ Fallback values:
103
123
  - Input: text and image
104
124
  - Reasoning: enabled
105
125
 
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.
126
+ The provider maps Pi's reasoning levels to Kimi's `reasoning_effort` and top-level `thinking` parameters. The mapping refreshes automatically on credential refresh. Opening `/kimi-settings` also re-discovers the latest model metadata.
107
127
 
108
128
  ## Optional tools
109
129
 
@@ -115,7 +135,9 @@ Inside Pi, run:
115
135
  /kimi-settings
116
136
  ```
117
137
 
118
- That command shows your current Kimi usage summary and lets you edit the home or project config. Changes apply to the active session tool set.
138
+ That command shows the current server-side model name (e.g. "K2.7 Code High Speed"), your Kimi usage summary, and lets you edit the home or project config. Changes apply to the active session tool set.
139
+
140
+ Configurable settings include protocol mode, upload threshold, and per-tool enable/collapse.
119
141
 
120
142
  Config files are JSON:
121
143
 
@@ -136,6 +158,8 @@ Project config overrides home config with a deep merge. Missing files or missing
136
158
 
137
159
  ```json
138
160
  {
161
+ "protocol": "openai",
162
+ "uploads": { "thresholdBytes": 1048576 },
139
163
  "tools": {
140
164
  "moonshot_search": { "enabled": true, "default_collapsed": true },
141
165
  "moonshot_fetch": { "enabled": true, "default_collapsed": true },
@@ -176,9 +200,9 @@ If you already use another search or fetch tool, pick one path for a session. Ov
176
200
  Most users do not need environment variables. Two are worth knowing:
177
201
 
178
202
  - `KIMI_API_KEY` — static API key for CI or pay-per-token use.
179
- - `KIMI_CODE_PROTOCOL` — `openai` by default; set to `anthropic` if your Pi setup needs Anthropic-compatible requests.
203
+ - `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.
180
204
 
181
- Moonshot search/fetch tools are configured through `/kimi-settings` or JSON config files, not env vars.
205
+ Tools, protocol, and upload threshold are all configurable through `/kimi-settings` or JSON config files.
182
206
 
183
207
  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).
184
208
 
@@ -263,6 +287,7 @@ In OpenAI mode this extension maps the `developer` role to `system` (Kimi's Codi
263
287
 
264
288
  - Pi: [earendil-works/pi](https://github.com/earendil-works/pi)
265
289
  - Environment variables: [docs/ENV.md](docs/ENV.md)
290
+ - Programmatic usage: [docs/programmatic-usage.md](docs/programmatic-usage.md)
266
291
  - Testing guide: [docs/TESTING.md](docs/TESTING.md)
267
292
  - Cache behavior: [docs/caching.md](docs/caching.md)
268
293
  - Architecture notes: [docs/architecture.md](docs/architecture.md)
package/index.ts CHANGED
@@ -18,17 +18,18 @@
18
18
  * src/stream.ts — empty-response filter + streamSimpleKimi orchestrator
19
19
  */
20
20
 
21
- import type { Api, Model } from "@earendil-works/pi-ai";
22
21
  import {
23
22
  AuthStorage,
24
23
  type ExtensionAPI,
25
24
  type ExtensionCommandContext,
25
+ type ExtensionFactory,
26
26
  } from "@earendil-works/pi-coding-agent";
27
27
  import os from "node:os";
28
28
  import { relative } from "node:path";
29
29
 
30
30
  import {
31
31
  type KimiCodeConfig,
32
+ type KimiCodeConfigPatch,
32
33
  KIMI_TOOL_NAMES,
33
34
  getProjectKimiCodeConfigPath,
34
35
  getGlobalKimiCodeConfigPath,
@@ -40,20 +41,18 @@ import {
40
41
  saveProjectKimiCodeConfig,
41
42
  type KimiToolName,
42
43
  } from "./src/config.ts";
43
- import {
44
- DEFAULT_KIMI_MODEL_INPUT,
45
- KIMI_API_TYPE,
46
- PROVIDER_ID,
47
- getBaseUrl,
48
- } from "./src/constants.ts";
44
+ import { PROVIDER_ID, PROVIDER_VERSION, getBaseUrl, getKimiApiType } from "./src/constants.ts";
49
45
  import { getCommonHeaders } from "./src/device.ts";
50
46
  import {
51
47
  type KimiOAuthCredentials,
52
- applyKimiEnvOverridesToModel,
48
+ type KimiOAuthExtras,
49
+ buildKimiModelFromConfig,
53
50
  applyKimiOAuthExtrasToModel,
51
+ discoverKimiModelMetadata,
52
+ resolveKimiModelConfig,
54
53
  } from "./src/models.ts";
55
54
  import { loginKimiCode, refreshKimiAuthToken, refreshKimiCodeToken } from "./src/oauth.ts";
56
- import { streamSimpleKimi } from "./src/stream.ts";
55
+ import { setStoreResolvedKimiConfig, streamSimpleKimi } from "./src/stream.ts";
57
56
  import { buildMoonshotFetchTool, buildMoonshotSearchTool } from "./src/tools/moonshot.ts";
58
57
  import { buildKimiDatasourceTool } from "./src/tools/datasource.ts";
59
58
  type KimiConfigScope = "project" | "home";
@@ -72,6 +71,13 @@ interface UsageRow {
72
71
  limit: number;
73
72
  }
74
73
 
74
+ interface KimiRuntimeState {
75
+ cwd: string;
76
+ config: KimiCodeConfig;
77
+ modelExtras: KimiOAuthExtras;
78
+ overrides?: KimiCodeConfigPatch;
79
+ }
80
+
75
81
  function buildKimiTool(toolName: KimiToolName, config: KimiCodeConfig) {
76
82
  const opts = { defaultCollapsed: config.tools[toolName].default_collapsed };
77
83
  if (toolName === "moonshot_search") return buildMoonshotSearchTool(opts);
@@ -105,6 +111,29 @@ function registerConfiguredMoonshotTools(
105
111
  pi.setActiveTools([...activeTools]);
106
112
  }
107
113
 
114
+ function reloadEffectiveKimiRuntimeConfig(state: KimiRuntimeState, cwd: string): KimiCodeConfig {
115
+ const config = loadKimiCodeConfig({ cwd, home: os.homedir() }, state.overrides);
116
+ state.cwd = cwd;
117
+ state.config = config;
118
+ setStoreResolvedKimiConfig({
119
+ model: resolveKimiModelConfig(config.model, state.modelExtras),
120
+ protocol: config.protocol,
121
+ uploads: config.uploads,
122
+ });
123
+ return config;
124
+ }
125
+
126
+ function applyEffectiveKimiRuntimeConfig(
127
+ pi: ExtensionAPI,
128
+ state: KimiRuntimeState,
129
+ cwd: string,
130
+ options: { updateActiveTools: boolean },
131
+ ): KimiCodeConfig {
132
+ const config = reloadEffectiveKimiRuntimeConfig(state, cwd);
133
+ registerConfiguredMoonshotTools(pi, config, options);
134
+ return config;
135
+ }
136
+
108
137
  function getKimiUsageToken(): string | null {
109
138
  const credential = AuthStorage.create().get(PROVIDER_ID);
110
139
  if (credential?.type === "oauth" && credential.access) return credential.access;
@@ -231,6 +260,7 @@ function moonshotStatus(config: KimiCodeConfig): string {
231
260
 
232
261
  function toggleEnabled(config: KimiCodeConfig, toolName: KimiToolName): KimiCodeConfig {
233
262
  return {
263
+ ...config,
234
264
  tools: {
235
265
  ...config.tools,
236
266
  [toolName]: {
@@ -243,6 +273,7 @@ function toggleEnabled(config: KimiCodeConfig, toolName: KimiToolName): KimiCode
243
273
 
244
274
  function toggleCollapsed(config: KimiCodeConfig, toolName: KimiToolName): KimiCodeConfig {
245
275
  return {
276
+ ...config,
246
277
  tools: {
247
278
  ...config.tools,
248
279
  [toolName]: {
@@ -253,13 +284,34 @@ function toggleCollapsed(config: KimiCodeConfig, toolName: KimiToolName): KimiCo
253
284
  };
254
285
  }
255
286
 
256
- async function runKimiCommand(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> {
257
- let config = loadKimiCodeConfig({ cwd: ctx.cwd, home: os.homedir() });
258
- let usage = await fetchKimiUsageSummary();
287
+ function setProtocol(config: KimiCodeConfig, protocol: KimiCodeConfig["protocol"]): KimiCodeConfig {
288
+ return { ...config, protocol };
289
+ }
290
+
291
+ function setUploadThreshold(config: KimiCodeConfig, thresholdBytes: number): KimiCodeConfig {
292
+ return { ...config, uploads: { ...config.uploads, thresholdBytes } };
293
+ }
294
+
295
+ async function refreshModelExtras(state: KimiRuntimeState): Promise<void> {
296
+ const token = getKimiUsageToken();
297
+ if (!token) return;
298
+ const extras = await discoverKimiModelMetadata(token, state.config.protocol);
299
+ if (Object.keys(extras).length > 0) {
300
+ Object.assign(state.modelExtras, extras);
301
+ }
302
+ }
303
+
304
+ async function runKimiCommand(
305
+ pi: ExtensionAPI,
306
+ ctx: ExtensionCommandContext,
307
+ state: KimiRuntimeState,
308
+ ): Promise<void> {
309
+ let config = applyEffectiveKimiRuntimeConfig(pi, state, ctx.cwd, { updateActiveTools: true });
310
+ let [usage] = await Promise.all([fetchKimiUsageSummary(), refreshModelExtras(state)]);
259
311
  ctx.ui.notify(usage);
260
312
 
261
313
  while (true) {
262
- const choice = await ctx.ui.select(buildKimiMainTitle(config, ctx.cwd), [
314
+ const choice = await ctx.ui.select(buildKimiMainTitle(config, ctx.cwd, state.modelExtras), [
263
315
  `Edit project config (${relative(ctx.cwd, getProjectKimiCodeConfigPath(ctx.cwd))})`,
264
316
  `Edit home config (${homeRelative(getGlobalKimiCodeConfigPath(os.homedir()))})`,
265
317
  "Refresh usage",
@@ -273,9 +325,9 @@ async function runKimiCommand(pi: ExtensionAPI, ctx: ExtensionCommandContext): P
273
325
  continue;
274
326
  }
275
327
  if (choice.startsWith("Edit project config")) {
276
- config = await editConfigScope(pi, ctx, "project");
328
+ config = await editConfigScope(pi, ctx, state, "project");
277
329
  } else if (choice.startsWith("Edit home config")) {
278
- config = await editConfigScope(pi, ctx, "home");
330
+ config = await editConfigScope(pi, ctx, state, "home");
279
331
  }
280
332
  }
281
333
  }
@@ -283,26 +335,75 @@ async function runKimiCommand(pi: ExtensionAPI, ctx: ExtensionCommandContext): P
283
335
  async function editConfigScope(
284
336
  pi: ExtensionAPI,
285
337
  ctx: ExtensionCommandContext,
338
+ state: KimiRuntimeState,
286
339
  scope: KimiConfigScope,
287
340
  ): Promise<KimiCodeConfig> {
288
341
  let current = loadScopeKimiCodeConfig(scope, ctx.cwd);
289
342
  while (true) {
290
343
  const items = KIMI_TOOL_NAMES.map((name) => toolMenuItem(current, name));
291
- items.push("Back");
344
+ items.push(protocolMenuItem(current), uploadThresholdMenuItem(current), "Back");
292
345
  const choice = await ctx.ui.select(buildConfigScopeTitle(scope, current, ctx.cwd), items);
293
346
  if (!choice || choice === "Back") {
294
347
  return loadKimiCodeConfig({ cwd: ctx.cwd, home: os.homedir() });
295
348
  }
296
349
  const toolName = KIMI_TOOL_NAMES.find((name) => choice.startsWith(name));
297
350
  if (toolName) {
298
- current = await editMoonshotTool(pi, ctx, scope, current, toolName);
351
+ current = await editMoonshotTool(pi, ctx, state, scope, current, toolName);
352
+ } else if (choice.startsWith("Protocol")) {
353
+ current = await editProtocol(pi, ctx, state, scope, current);
354
+ } else if (choice.startsWith("Upload threshold")) {
355
+ current = await editUploadThreshold(pi, ctx, state, scope, current);
299
356
  }
300
357
  }
301
358
  }
302
359
 
360
+ async function editProtocol(
361
+ pi: ExtensionAPI,
362
+ ctx: ExtensionCommandContext,
363
+ state: KimiRuntimeState,
364
+ scope: KimiConfigScope,
365
+ config: KimiCodeConfig,
366
+ ): Promise<KimiCodeConfig> {
367
+ const choice = await ctx.ui.select(`Edit protocol\n\n${formatProtocolStatus(config)}`, [
368
+ "Use openai protocol",
369
+ "Use anthropic protocol",
370
+ "Back",
371
+ ]);
372
+ if (!choice || choice === "Back") return config;
373
+ const protocol = choice.includes("anthropic") ? "anthropic" : "openai";
374
+ const current = setProtocol(config, protocol);
375
+ saveAndApplyKimiCodeConfig(pi, ctx, state, scope, current);
376
+ ctx.ui.notify("Saved protocol config", "info");
377
+ return current;
378
+ }
379
+
380
+ async function editUploadThreshold(
381
+ pi: ExtensionAPI,
382
+ ctx: ExtensionCommandContext,
383
+ state: KimiRuntimeState,
384
+ scope: KimiConfigScope,
385
+ config: KimiCodeConfig,
386
+ ): Promise<KimiCodeConfig> {
387
+ const input = await ctx.ui.input(
388
+ "Edit upload threshold\n\nExamples: 512 KiB, 2 MiB, 1.5 MB. Plain numbers are MiB.",
389
+ formatByteSize(config.uploads.thresholdBytes),
390
+ );
391
+ if (input === undefined) return config;
392
+ const thresholdBytes = parseByteSizeInput(input);
393
+ if (thresholdBytes === undefined || thresholdBytes <= 0) {
394
+ ctx.ui.notify("Upload threshold must be a positive size, for example 2 MiB", "error");
395
+ return config;
396
+ }
397
+ const current = setUploadThreshold(config, thresholdBytes);
398
+ saveAndApplyKimiCodeConfig(pi, ctx, state, scope, current);
399
+ ctx.ui.notify("Saved upload threshold config", "info");
400
+ return current;
401
+ }
402
+
303
403
  async function editMoonshotTool(
304
404
  pi: ExtensionAPI,
305
405
  ctx: ExtensionCommandContext,
406
+ state: KimiRuntimeState,
306
407
  scope: KimiConfigScope,
307
408
  config: KimiCodeConfig,
308
409
  toolName: KimiToolName,
@@ -324,14 +425,23 @@ async function editMoonshotTool(
324
425
  } else if (choice.endsWith("previews by default")) {
325
426
  current = toggleCollapsed(current, toolName);
326
427
  }
327
- saveScopeKimiCodeConfig(scope, ctx.cwd, current);
328
- const effective = loadKimiCodeConfig({ cwd: ctx.cwd, home: os.homedir() });
329
- registerConfiguredMoonshotTools(pi, effective, { updateActiveTools: true });
428
+ saveAndApplyKimiCodeConfig(pi, ctx, state, scope, current);
330
429
  ctx.ui.notify(`Saved ${toolName} config`, "info");
331
430
  return current;
332
431
  }
333
432
  }
334
433
 
434
+ function saveAndApplyKimiCodeConfig(
435
+ pi: ExtensionAPI,
436
+ ctx: ExtensionCommandContext,
437
+ state: KimiRuntimeState,
438
+ scope: KimiConfigScope,
439
+ config: KimiCodeConfig,
440
+ ): void {
441
+ saveScopeKimiCodeConfig(scope, ctx.cwd, config);
442
+ applyEffectiveKimiRuntimeConfig(pi, state, ctx.cwd, { updateActiveTools: true });
443
+ }
444
+
335
445
  function loadScopeKimiCodeConfig(scope: KimiConfigScope, cwd: string): KimiCodeConfig {
336
446
  if (scope === "project") return loadProjectKimiCodeConfig(cwd);
337
447
  return loadHomeKimiCodeConfig(os.homedir());
@@ -349,10 +459,15 @@ function saveScopeKimiCodeConfig(
349
459
  }
350
460
  }
351
461
 
352
- function buildKimiMainTitle(config: KimiCodeConfig, cwd: string): string {
462
+ function buildKimiMainTitle(config: KimiCodeConfig, cwd: string, extras: KimiOAuthExtras): string {
353
463
  const sources = loadKimiCodeConfigSources({ cwd, home: os.homedir() });
464
+ const modelName = extras.modelDisplay || "kimi-for-coding";
354
465
  return [
355
- "Kimi settings",
466
+ `Kimi settings (provider v${PROVIDER_VERSION})`,
467
+ "",
468
+ `Model: ${modelName}`,
469
+ `Protocol: ${config.protocol} (${sources.protocol})`,
470
+ `Upload threshold: ${formatByteSize(config.uploads.thresholdBytes)} (${sources.uploads.thresholdBytes})`,
356
471
  "",
357
472
  "Effective tools:",
358
473
  ...KIMI_TOOL_NAMES.map((toolName) => {
@@ -371,7 +486,15 @@ function buildConfigScopeTitle(
371
486
  scope === "project"
372
487
  ? relative(cwd, getProjectKimiCodeConfigPath(cwd))
373
488
  : homeRelative(getGlobalKimiCodeConfigPath(os.homedir()));
374
- return [`Edit ${scope} config`, `File: ${path}`, "", moonshotStatus(config)].join("\n");
489
+ return [
490
+ `Edit ${scope} config`,
491
+ `File: ${path}`,
492
+ "",
493
+ formatProtocolStatus(config),
494
+ formatUploadThresholdStatus(config),
495
+ "",
496
+ moonshotStatus(config),
497
+ ].join("\n");
375
498
  }
376
499
 
377
500
  function homeRelative(filePath: string): string {
@@ -383,6 +506,64 @@ function toolMenuItem(config: KimiCodeConfig, toolName: KimiToolName): string {
383
506
  return `${toolName} -> ${formatToolStatus(config, toolName)}`;
384
507
  }
385
508
 
509
+ function protocolMenuItem(config: KimiCodeConfig): string {
510
+ return `Protocol -> ${config.protocol}`;
511
+ }
512
+
513
+ function uploadThresholdMenuItem(config: KimiCodeConfig): string {
514
+ return `Upload threshold -> ${formatByteSize(config.uploads.thresholdBytes)}`;
515
+ }
516
+
517
+ function formatByteSize(bytes: number): string {
518
+ const units = [
519
+ { suffix: "GiB", size: 1024 ** 3 },
520
+ { suffix: "MiB", size: 1024 ** 2 },
521
+ { suffix: "KiB", size: 1024 },
522
+ ];
523
+ for (const unit of units) {
524
+ if (bytes >= unit.size && bytes % unit.size === 0) {
525
+ return `${bytes / unit.size} ${unit.suffix}`;
526
+ }
527
+ }
528
+ for (const unit of units) {
529
+ if (bytes >= unit.size) {
530
+ return `${(bytes / unit.size).toFixed(2).replace(/\.00$/, "")} ${unit.suffix}`;
531
+ }
532
+ }
533
+ return `${bytes} B`;
534
+ }
535
+
536
+ function parseByteSizeInput(input: string): number | undefined {
537
+ const match = input.trim().match(/^(\d+(?:\.\d+)?)\s*(b|bytes?|kib|kb|mib|mb|gib|gb)?$/i);
538
+ if (!match) return undefined;
539
+ const value = Number(match[1]);
540
+ if (!Number.isFinite(value) || value <= 0) return undefined;
541
+ const unit = (match[2] ?? "mib").toLowerCase();
542
+ const multipliers: Record<string, number> = {
543
+ b: 1,
544
+ byte: 1,
545
+ bytes: 1,
546
+ kib: 1024,
547
+ kb: 1000,
548
+ mib: 1024 ** 2,
549
+ mb: 1000 ** 2,
550
+ gib: 1024 ** 3,
551
+ gb: 1000 ** 3,
552
+ };
553
+ const multiplier = multipliers[unit];
554
+ if (!multiplier) return undefined;
555
+ const bytes = Math.round(value * multiplier);
556
+ return Number.isSafeInteger(bytes) && bytes > 0 ? bytes : undefined;
557
+ }
558
+
559
+ function formatProtocolStatus(config: KimiCodeConfig): string {
560
+ return `protocol: ${config.protocol}`;
561
+ }
562
+
563
+ function formatUploadThresholdStatus(config: KimiCodeConfig): string {
564
+ return `upload threshold: ${formatByteSize(config.uploads.thresholdBytes)}`;
565
+ }
566
+
386
567
  function formatToolStatus(config: KimiCodeConfig, toolName: KimiToolName): string {
387
568
  const tool = config.tools[toolName];
388
569
  const enabled = tool.enabled ? "enabled" : "disabled";
@@ -390,53 +571,58 @@ function formatToolStatus(config: KimiCodeConfig, toolName: KimiToolName): strin
390
571
  return `${enabled}, ${collapsed}`;
391
572
  }
392
573
 
393
- export default function (pi: ExtensionAPI) {
394
- const config = loadKimiCodeConfig({ cwd: process.cwd(), home: os.homedir() });
395
-
396
- pi.registerProvider(PROVIDER_ID, {
397
- baseUrl: getBaseUrl(),
398
- apiKey: "$KIMI_API_KEY",
399
- api: KIMI_API_TYPE,
400
- streamSimple: streamSimpleKimi,
401
-
402
- models: [
403
- applyKimiEnvOverridesToModel({
404
- id: "kimi-for-coding",
405
- name: "Kimi for Coding",
406
- reasoning: true,
407
- input: [...DEFAULT_KIMI_MODEL_INPUT] as unknown as ("text" | "image")[],
408
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
409
- contextWindow: 262144,
410
- maxTokens: 32000,
411
- } as Model<Api>),
412
- ],
413
-
414
- oauth: {
415
- name: "Kimi Code (OAuth)",
416
- login: loginKimiCode,
417
- refreshToken: refreshKimiCodeToken,
418
- getApiKey: (cred) => cred.access,
419
- // Reflect server-side model identity on the registered model after login
420
- // / refresh. We never rewrite the model id (pi-side `/model` selections
421
- // and persisted sessions reference it); only the human-facing name, the
422
- // context window, and an out-of-band `wireModelId` carried into the
423
- // request payload by streamSimpleKimi.
424
- modifyModels: (models, cred) => {
425
- const extras = cred as KimiOAuthCredentials;
426
- return models.map((model) => {
427
- if (model.id !== "kimi-for-coding") return model;
428
- return applyKimiEnvOverridesToModel(applyKimiOAuthExtrasToModel(model, extras));
429
- });
574
+ export function KimiCode(overrides?: KimiCodeConfigPatch): ExtensionFactory {
575
+ return async (pi: ExtensionAPI) => {
576
+ const cwd = process.cwd();
577
+ const config = loadKimiCodeConfig({ cwd, home: os.homedir() }, overrides);
578
+ const baseModel = buildKimiModelFromConfig(config.model);
579
+ const discoveryToken = getKimiUsageToken();
580
+ const discovered = discoveryToken
581
+ ? await discoverKimiModelMetadata(discoveryToken, config.protocol)
582
+ : {};
583
+ const state: KimiRuntimeState = { cwd, config, modelExtras: discovered, overrides };
584
+ reloadEffectiveKimiRuntimeConfig(state, cwd);
585
+ const model = applyKimiOAuthExtrasToModel(baseModel, discovered);
586
+
587
+ pi.registerProvider(PROVIDER_ID, {
588
+ baseUrl: getBaseUrl(config.protocol),
589
+ apiKey: "$KIMI_API_KEY",
590
+ api: getKimiApiType(config.protocol),
591
+ streamSimple: streamSimpleKimi,
592
+
593
+ models: [model],
594
+
595
+ oauth: {
596
+ name: "Kimi Code (OAuth)",
597
+ login: loginKimiCode,
598
+ refreshToken: refreshKimiCodeToken,
599
+ getApiKey: (cred) => cred.access,
600
+ // Reflect server-side model identity on the registered model after login
601
+ // / refresh. We never rewrite the model id (pi-side `/model` selections
602
+ // and persisted sessions reference it); only the human-facing name, the
603
+ // context window, and an out-of-band `wireModelId` carried into the
604
+ // request payload by streamSimpleKimi.
605
+ modifyModels: (models, cred) => {
606
+ const extras = cred as KimiOAuthCredentials;
607
+ state.modelExtras = extras;
608
+ reloadEffectiveKimiRuntimeConfig(state, state.cwd);
609
+ return models.map((model) => {
610
+ if (model.id !== "kimi-for-coding") return model;
611
+ return applyKimiOAuthExtrasToModel(model, extras);
612
+ });
613
+ },
430
614
  },
431
- },
432
- });
615
+ });
433
616
 
434
- registerConfiguredMoonshotTools(pi, config, { updateActiveTools: false });
617
+ registerConfiguredMoonshotTools(pi, state.config, { updateActiveTools: false });
435
618
 
436
- pi.registerCommand("kimi-settings", {
437
- description: "Show Kimi usage and configure optional Kimi tools",
438
- handler: async (_args, ctx) => {
439
- await runKimiCommand(pi, ctx);
440
- },
441
- });
619
+ pi.registerCommand("kimi-settings", {
620
+ description: "Show Kimi usage and configure optional Kimi tools",
621
+ handler: async (_args, ctx) => {
622
+ await runKimiCommand(pi, ctx, state);
623
+ },
624
+ });
625
+ };
442
626
  }
627
+
628
+ export default KimiCode();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-kimi-code",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Use Kimi Code in Pi without losing the Kimi-specific bits",
5
5
  "keywords": [
6
6
  "ai-provider",