pi-provider-kimi-code 0.1.7 → 0.1.8

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.
Files changed (3) hide show
  1. package/README.md +16 -6
  2. package/index.ts +72 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -38,21 +38,31 @@ KIMI_API_KEY=sk-... pi
38
38
 
39
39
  ## Models
40
40
 
41
- | ID | Name | Reasoning | Input | Context | Max Output |
42
- |--------------------|------------------|-----------|-------------|----------|------------|
43
- | `kimi-k2-thinking` | Kimi K2 Thinking | yes | text | 262 144 | 32 768 |
44
- | `k2p5` | Kimi K2.5 | yes | text, image | 262 144 | 32 768 |
41
+ | ID | Name | Reasoning | Input | Context | Max Output |
42
+ |---------------------------|--------------------------------|-----------|-------------|----------|------------|
43
+ | `kimi-code` | Kimi Code (powered by kimi-k2.5) | yes | text, image | 262 144 | 32 768 |
44
+ | `kimi-k2.5` | Kimi K2.5 | yes | text, image | 262 144 | 32 768 |
45
+ | `kimi-k2-thinking-turbo` | Kimi K2 Thinking Turbo | yes | text | 262 144 | 32 768 |
45
46
 
46
47
  Select a model inside pi:
47
48
 
48
49
  ```
49
- /model kimi-coding/kimi-k2-thinking
50
+ /model kimi-coding/kimi-code
50
51
  ```
51
52
 
53
+ ## Environment Overrides
54
+
55
+ - `KIMI_CODE_BASE_URL` — override the default API base URL (`https://api.kimi.com/coding`)
56
+ - `KIMI_CODE_OAUTH_HOST` — override the OAuth host
57
+ - `KIMI_OAUTH_HOST` — fallback OAuth host override for compatibility
58
+
52
59
  ## How It Works
53
60
 
54
61
  - Registers provider `kimi-coding` with base URL `https://api.kimi.com/coding`
55
- - Uses `api: "anthropic-messages"` — Kimi's API is wire-compatible with the Anthropic Messages format, so no custom streaming logic is needed
62
+ - Uses `api: "anthropic-messages"` — Kimi's API is wire-compatible with the Anthropic Messages format
63
+ - Adds a small `streamSimple` wrapper to suppress Kimi's leaked `(Empty response: ...)` placeholder blocks
64
+ - Sends the same `KimiCLI/1.28.0` + `X-Msh-*` headers as current `kimi-cli`
65
+ - Persists a stable device ID at `~/.pi/providers/kimi-coding/device_id`
56
66
  - OAuth uses [RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628) device authorization grant against `https://auth.kimi.com`
57
67
  - Zero dependencies — types from `@mariozechner/pi-ai` and `@mariozechner/pi-coding-agent` are provided by the pi runtime
58
68
  - Zero build step — pi loads TypeScript directly via jiti
package/index.ts CHANGED
@@ -10,7 +10,9 @@
10
10
  */
11
11
 
12
12
  import { randomBytes } from "node:crypto";
13
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
14
  import os from "node:os";
15
+ import { dirname, join } from "node:path";
14
16
  import type { OAuthCredentials, OAuthLoginCallbacks, AssistantMessageEvent, Context, Model, SimpleStreamOptions } from "@mariozechner/pi-ai";
15
17
  import { streamSimpleAnthropic, AssistantMessageEventStream } from "@mariozechner/pi-ai";
16
18
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
@@ -20,19 +22,59 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
20
22
  // =============================================================================
21
23
 
22
24
  const CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
23
- const OAUTH_HOST = "https://auth.kimi.com";
24
- const KIMI_CLI_USER_AGENT = "KimiCLI/1.0.0";
25
+ const DEFAULT_OAUTH_HOST = "https://auth.kimi.com";
26
+ const DEFAULT_BASE_URL = "https://api.kimi.com/coding";
27
+ const KIMI_CLI_VERSION = "1.28.0";
28
+ const KIMI_CLI_USER_AGENT = `KimiCLI/${KIMI_CLI_VERSION}`;
25
29
  const KIMI_PLATFORM = "kimi_cli";
26
- const KIMI_CLI_VERSION = "1.0.0";
30
+ const DEVICE_ID_PATH = join(os.homedir(), ".pi", "providers", "kimi-coding", "device_id");
27
31
 
28
32
  // =============================================================================
29
33
  // Device identification
30
34
  // =============================================================================
31
35
 
36
+ function getOAuthHost(): string {
37
+ const value = process.env.KIMI_CODE_OAUTH_HOST || process.env.KIMI_OAUTH_HOST || DEFAULT_OAUTH_HOST;
38
+ return value.trim() || DEFAULT_OAUTH_HOST;
39
+ }
40
+
41
+ function getBaseUrl(): string {
42
+ const value = process.env.KIMI_CODE_BASE_URL || DEFAULT_BASE_URL;
43
+ return value.trim() || DEFAULT_BASE_URL;
44
+ }
45
+
32
46
  function createDeviceId(): string {
33
47
  return randomBytes(16).toString("hex");
34
48
  }
35
49
 
50
+ function ensurePrivateFile(path: string): void {
51
+ try {
52
+ chmodSync(path, 0o600);
53
+ } catch {
54
+ // Ignore chmod failures on platforms/filesystems that do not support it.
55
+ }
56
+ }
57
+
58
+ function readPersistedDeviceId(): string | null {
59
+ try {
60
+ if (!existsSync(DEVICE_ID_PATH)) return null;
61
+ const deviceId = readFileSync(DEVICE_ID_PATH, "utf8").trim();
62
+ return deviceId || null;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function persistDeviceId(deviceId: string): void {
69
+ try {
70
+ mkdirSync(dirname(DEVICE_ID_PATH), { recursive: true });
71
+ writeFileSync(DEVICE_ID_PATH, deviceId, "utf8");
72
+ ensurePrivateFile(DEVICE_ID_PATH);
73
+ } catch {
74
+ // Ignore persistence failures and fall back to the in-memory device id.
75
+ }
76
+ }
77
+
36
78
  function getDeviceModel(): string {
37
79
  const platform = process.platform;
38
80
  const arch = os.machine() || process.arch;
@@ -46,18 +88,36 @@ function getDeviceModel(): string {
46
88
  return release && arch ? `${platform} ${release} ${arch}` : `${platform} ${arch}`;
47
89
  }
48
90
 
91
+ function asciiHeaderValue(value: string, fallback = "unknown"): string {
92
+ const trimmed = value.trim();
93
+ if (/^[\x00-\x7F]*$/.test(trimmed)) {
94
+ return trimmed;
95
+ }
96
+ const sanitized = trimmed.replace(/[^\x00-\x7F]/g, "").trim();
97
+ return sanitized || fallback;
98
+ }
99
+
49
100
  const DEVICE_MODEL = getDeviceModel();
50
101
  let DEVICE_ID: string | null = null;
51
102
 
52
103
  function getStableDeviceId(): string {
53
- if (!DEVICE_ID) {
54
- DEVICE_ID = createDeviceId();
104
+ if (DEVICE_ID) {
105
+ return DEVICE_ID;
106
+ }
107
+
108
+ const persisted = readPersistedDeviceId();
109
+ if (persisted) {
110
+ DEVICE_ID = persisted;
111
+ return DEVICE_ID;
55
112
  }
113
+
114
+ DEVICE_ID = createDeviceId();
115
+ persistDeviceId(DEVICE_ID);
56
116
  return DEVICE_ID;
57
117
  }
58
118
 
59
119
  function getCommonHeaders(): Record<string, string> {
60
- return {
120
+ const headers = {
61
121
  "User-Agent": KIMI_CLI_USER_AGENT,
62
122
  "X-Msh-Platform": KIMI_PLATFORM,
63
123
  "X-Msh-Version": KIMI_CLI_VERSION,
@@ -66,6 +126,7 @@ function getCommonHeaders(): Record<string, string> {
66
126
  "X-Msh-Os-Version": os.release(),
67
127
  "X-Msh-Device-Id": getStableDeviceId(),
68
128
  };
129
+ return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, asciiHeaderValue(value)])) as Record<string, string>;
69
130
  }
70
131
 
71
132
  // =============================================================================
@@ -90,7 +151,7 @@ interface TokenResponse {
90
151
  }
91
152
 
92
153
  async function requestDeviceAuthorization(): Promise<DeviceAuthorization> {
93
- const response = await fetch(`${OAUTH_HOST}/api/oauth/device_authorization`, {
154
+ const response = await fetch(`${getOAuthHost()}/api/oauth/device_authorization`, {
94
155
  method: "POST",
95
156
  headers: {
96
157
  "Content-Type": "application/x-www-form-urlencoded",
@@ -130,7 +191,7 @@ async function requestDeviceAuthorization(): Promise<DeviceAuthorization> {
130
191
  }
131
192
 
132
193
  async function requestDeviceToken(auth: DeviceAuthorization): Promise<TokenResponse | null> {
133
- const response = await fetch(`${OAUTH_HOST}/api/oauth/token`, {
194
+ const response = await fetch(`${getOAuthHost()}/api/oauth/token`, {
134
195
  method: "POST",
135
196
  headers: {
136
197
  "Content-Type": "application/x-www-form-urlencoded",
@@ -154,7 +215,7 @@ async function requestDeviceToken(auth: DeviceAuthorization): Promise<TokenRespo
154
215
  if (response.status === 400) {
155
216
  const data = (await response.json()) as { error?: string; error_description?: string };
156
217
  if (data.error === "authorization_pending") {
157
- return null; // Still waiting for user
218
+ return null;
158
219
  }
159
220
  if (data.error === "expired_token") {
160
221
  throw new Error("expired_token");
@@ -167,7 +228,7 @@ async function requestDeviceToken(auth: DeviceAuthorization): Promise<TokenRespo
167
228
  }
168
229
 
169
230
  async function refreshAccessToken(refreshToken: string): Promise<TokenResponse> {
170
- const response = await fetch(`${OAUTH_HOST}/api/oauth/token`, {
231
+ const response = await fetch(`${getOAuthHost()}/api/oauth/token`, {
171
232
  method: "POST",
172
233
  headers: {
173
234
  "Content-Type": "application/x-www-form-urlencoded",
@@ -356,7 +417,7 @@ function streamSimpleKimi(
356
417
 
357
418
  export default function (pi: ExtensionAPI) {
358
419
  pi.registerProvider("kimi-coding", {
359
- baseUrl: "https://api.kimi.com/coding",
420
+ baseUrl: getBaseUrl(),
360
421
  apiKey: "KIMI_API_KEY",
361
422
  api: "anthropic-messages",
362
423
  streamSimple: streamSimpleKimi,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-kimi-code",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "pi custom provider extension for Kimi Code models with OAuth device-code login",
5
5
  "type": "module",
6
6
  "license": "MIT",