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.
- package/README.md +16 -6
- package/index.ts +72 -11
- 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
|
|
42
|
-
|
|
43
|
-
| `kimi-
|
|
44
|
-
| `
|
|
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-
|
|
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
|
|
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
|
|
24
|
-
const
|
|
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
|
|
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 (
|
|
54
|
-
DEVICE_ID
|
|
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
|
-
|
|
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(`${
|
|
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(`${
|
|
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;
|
|
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(`${
|
|
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:
|
|
420
|
+
baseUrl: getBaseUrl(),
|
|
360
421
|
apiKey: "KIMI_API_KEY",
|
|
361
422
|
api: "anthropic-messages",
|
|
362
423
|
streamSimple: streamSimpleKimi,
|