pi-provider-kimi-code 0.1.7 → 0.1.9
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 +86 -7
- package/index.ts +570 -314
- package/package.json +20 -11
package/README.md
CHANGED
|
@@ -38,21 +38,100 @@ 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 000 |
|
|
44
|
+
| `kimi-k2.5` | Kimi K2.5 | yes | text, image | 262 144 | 32 000 |
|
|
45
|
+
| `kimi-k2-thinking-turbo` | Kimi K2 Thinking Turbo | yes | text | 262 144 | 32 000 |
|
|
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 (default depends on protocol; see below)
|
|
56
|
+
- `KIMI_CODE_OAUTH_HOST` — override the OAuth host
|
|
57
|
+
- `KIMI_OAUTH_HOST` — fallback OAuth host override for compatibility
|
|
58
|
+
- `KIMI_CODE_PROTOCOL` — choose protocol mode:
|
|
59
|
+
- unset / `anthropic` → `anthropic-messages` via `/coding/v1/messages`
|
|
60
|
+
- `openai` → `openai-completions` via `/coding/v1/chat/completions`
|
|
61
|
+
- `KIMI_MODEL_TEMPERATURE` — force temperature on outbound requests
|
|
62
|
+
- `KIMI_MODEL_TOP_P` — force top-p on outbound requests
|
|
63
|
+
- `KIMI_MODEL_MAX_TOKENS` — force max tokens on outbound requests
|
|
64
|
+
- `KIMI_CODE_DEBUG=1` — print provider-side debug logs (request metadata, file upload logs)
|
|
65
|
+
|
|
66
|
+
## Prompt Caching
|
|
67
|
+
|
|
68
|
+
Kimi Code API requires a `prompt_cache_key` field at the payload root for prompt caching. The pi framework separately injects Anthropic-style `cache_control: { type: "ephemeral" }` markers. This extension bridges both:
|
|
69
|
+
|
|
70
|
+
1. **Session key injection**: Extracts your current pi `sessionId` and injects it as `prompt_cache_key` on every request.
|
|
71
|
+
2. **TTL**: Approximately **5-10 minutes**. Consistently hits at 300s, borderline at 600s, misses at 900s. Measure in your environment with `KIMI_E2E_ONLY_CACHE=1` (see [E2E test script](#end-to-end-test-script)).
|
|
72
|
+
3. **Manual override**: Set `prompt_cache_key` explicitly in the payload or options to override the automatic session-based key.
|
|
73
|
+
|
|
74
|
+
Cache usage is easiest to verify on the Anthropic-compatible endpoint, which returns `cache_read_input_tokens` / `cache_creation_input_tokens` fields.
|
|
75
|
+
|
|
76
|
+
## OpenAI Compatibility Mode
|
|
77
|
+
|
|
78
|
+
Set `KIMI_CODE_PROTOCOL=openai` to switch the provider to Kimi Coding's OpenAI-compatible chat completions endpoint. Internally this extension uses `openai-completions` (not `openai-responses`) and applies Kimi-specific compatibility overrides:
|
|
79
|
+
|
|
80
|
+
- Maps `developer` role to `system` (Kimi does not recognize the OpenAI `developer` role)
|
|
81
|
+
- Disables `store`
|
|
82
|
+
- Uses `max_tokens`
|
|
83
|
+
- Sends `KimiCLI/*` + `X-Msh-*` agent headers required by the Coding endpoint
|
|
84
|
+
|
|
85
|
+
## End-to-End Test Script
|
|
86
|
+
|
|
87
|
+
This repo includes a tracked smoke-test script:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
KIMI_API_KEY=sk-... ./test_e2e.sh
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
By default it explicitly targets `--model kimi-coding/kimi-code`, disables extension discovery with `-ne`, and turns on provider debug logging.
|
|
94
|
+
|
|
95
|
+
What it does:
|
|
96
|
+
|
|
97
|
+
1. Runs a `pi -ne -e <extension-dir>` smoke test in Anthropic mode.
|
|
98
|
+
2. Runs the same smoke test in OpenAI mode.
|
|
99
|
+
3. Runs a high-thinking smoke test (saves full JSONL, extracts thinking + answer + usage).
|
|
100
|
+
4. Runs a prompt-cache TTL probe and prints a conclusion on observed cache lifetime.
|
|
101
|
+
5. Uploads a watermarked image to `/files`, then asks the model to read back the watermark via `ms://` reference.
|
|
102
|
+
6. Sends a `prompt_cache_key` request directly to verify cache key injection.
|
|
103
|
+
|
|
104
|
+
Useful environment variables for the test script:
|
|
105
|
+
|
|
106
|
+
| Variable | Default | Description |
|
|
107
|
+
| -------------------------- | ----------------------- | --------------------------------------- |
|
|
108
|
+
| `KIMI_API_KEY` | (required) | API key (or pass as positional arg) |
|
|
109
|
+
| `KIMI_CODE_DEBUG` | `1` | Provider debug logs |
|
|
110
|
+
| `KIMI_E2E_VERBOSE` | `1` | Command and environment diagnostics |
|
|
111
|
+
| `KIMI_E2E_MODEL` | `kimi-coding/kimi-code` | Model for `pi` smoke tests |
|
|
112
|
+
| `KIMI_E2E_CACHE_INTERVALS` | `60,300` | Absolute seconds from warmup per probe |
|
|
113
|
+
| `KIMI_E2E_CACHE_KEY` | auto | Override cache key for TTL probe |
|
|
114
|
+
| `KIMI_E2E_CACHE_REPEAT` | `2000` | Long-text repeat count for cache warmup |
|
|
115
|
+
| `KIMI_E2E_SKIP_CACHE` | `0` | Set `1` to skip the cache TTL phase |
|
|
116
|
+
| `KIMI_E2E_ONLY_CACHE` | `0` | Set `1` to run only the cache TTL test |
|
|
117
|
+
|
|
118
|
+
### Proxy / networking note
|
|
119
|
+
|
|
120
|
+
If `curl` can reach Kimi but `pi` reports `fetch failed`, check your `http_proxy` / `https_proxy` / `all_proxy` environment. `pi` runs on Node's `fetch` / undici stack, which may behave differently from `curl`; the test script prints the effective proxy-related environment for easier debugging.
|
|
121
|
+
|
|
52
122
|
## How It Works
|
|
53
123
|
|
|
54
|
-
- Registers provider `kimi-coding` with
|
|
55
|
-
-
|
|
124
|
+
- Registers provider `kimi-coding` with Kimi's API endpoint
|
|
125
|
+
- Supports two protocol modes:
|
|
126
|
+
- `anthropic-messages` (default)
|
|
127
|
+
- `openai-completions` when `KIMI_CODE_PROTOCOL=openai`
|
|
128
|
+
- Adds a small `streamSimple` wrapper to suppress Kimi's leaked `(Empty response: ...)` placeholder blocks
|
|
129
|
+
- Injects `prompt_cache_key` automatically so Kimi's cache works with pi sessions
|
|
130
|
+
- Maps pi thinking levels to Kimi-native `reasoning_effort` + `extra_body.thinking`
|
|
131
|
+
- Pre-uploads large image / video attachments to Kimi Files API (returns `ms://` references)
|
|
132
|
+
- Maps `developer` role to `system` for OpenAI protocol compatibility
|
|
133
|
+
- Sends the same `KimiCLI/1.30.0` + `X-Msh-*` headers as current `kimi-cli`
|
|
134
|
+
- Persists a stable device ID at `~/.pi/providers/kimi-coding/device_id`
|
|
56
135
|
- OAuth uses [RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628) device authorization grant against `https://auth.kimi.com`
|
|
57
136
|
- Zero dependencies — types from `@mariozechner/pi-ai` and `@mariozechner/pi-coding-agent` are provided by the pi runtime
|
|
58
137
|
- Zero build step — pi loads TypeScript directly via jiti
|
package/index.ts
CHANGED
|
@@ -9,10 +9,24 @@
|
|
|
9
9
|
* # Then /login kimi-coding, or set KIMI_API_KEY=...
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { execSync } from "node:child_process";
|
|
12
13
|
import { randomBytes } from "node:crypto";
|
|
14
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
15
|
import os from "node:os";
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import type {
|
|
18
|
+
OAuthCredentials,
|
|
19
|
+
OAuthLoginCallbacks,
|
|
20
|
+
AssistantMessageEvent,
|
|
21
|
+
Context,
|
|
22
|
+
Model,
|
|
23
|
+
SimpleStreamOptions,
|
|
24
|
+
} from "@mariozechner/pi-ai";
|
|
25
|
+
import {
|
|
26
|
+
streamSimpleAnthropic,
|
|
27
|
+
streamSimpleOpenAICompletions,
|
|
28
|
+
AssistantMessageEventStream,
|
|
29
|
+
} from "@mariozechner/pi-ai";
|
|
16
30
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
17
31
|
|
|
18
32
|
// =============================================================================
|
|
@@ -20,52 +34,131 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
|
20
34
|
// =============================================================================
|
|
21
35
|
|
|
22
36
|
const CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
|
|
23
|
-
const
|
|
24
|
-
const
|
|
37
|
+
const DEFAULT_OAUTH_HOST = "https://auth.kimi.com";
|
|
38
|
+
const PROTOCOL =
|
|
39
|
+
process.env.KIMI_CODE_PROTOCOL === "openai" ? "openai-completions" : "anthropic-messages";
|
|
40
|
+
const DEFAULT_BASE_URL =
|
|
41
|
+
PROTOCOL === "openai-completions"
|
|
42
|
+
? "https://api.kimi.com/coding/v1"
|
|
43
|
+
: "https://api.kimi.com/coding";
|
|
44
|
+
const KIMI_CLI_VERSION = "1.30.0";
|
|
45
|
+
const KIMI_CLI_USER_AGENT = `KimiCLI/${KIMI_CLI_VERSION}`;
|
|
25
46
|
const KIMI_PLATFORM = "kimi_cli";
|
|
26
|
-
const
|
|
47
|
+
const DEVICE_ID_PATH = join(os.homedir(), ".pi", "providers", "kimi-coding", "device_id");
|
|
27
48
|
|
|
28
49
|
// =============================================================================
|
|
29
50
|
// Device identification
|
|
30
51
|
// =============================================================================
|
|
31
52
|
|
|
53
|
+
function getOAuthHost(): string {
|
|
54
|
+
const value =
|
|
55
|
+
process.env.KIMI_CODE_OAUTH_HOST || process.env.KIMI_OAUTH_HOST || DEFAULT_OAUTH_HOST;
|
|
56
|
+
return value.trim() || DEFAULT_OAUTH_HOST;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getBaseUrl(): string {
|
|
60
|
+
const value = process.env.KIMI_CODE_BASE_URL || DEFAULT_BASE_URL;
|
|
61
|
+
return value.trim() || DEFAULT_BASE_URL;
|
|
62
|
+
}
|
|
63
|
+
|
|
32
64
|
function createDeviceId(): string {
|
|
33
|
-
|
|
65
|
+
return randomBytes(16).toString("hex");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function ensurePrivateFile(path: string): void {
|
|
69
|
+
try {
|
|
70
|
+
chmodSync(path, 0o600);
|
|
71
|
+
} catch {
|
|
72
|
+
// Ignore chmod failures on platforms/filesystems that do not support it.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readPersistedDeviceId(): string | null {
|
|
77
|
+
try {
|
|
78
|
+
if (!existsSync(DEVICE_ID_PATH)) return null;
|
|
79
|
+
const deviceId = readFileSync(DEVICE_ID_PATH, "utf8").trim();
|
|
80
|
+
return deviceId || null;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function persistDeviceId(deviceId: string): void {
|
|
87
|
+
try {
|
|
88
|
+
mkdirSync(dirname(DEVICE_ID_PATH), { recursive: true });
|
|
89
|
+
writeFileSync(DEVICE_ID_PATH, deviceId, "utf8");
|
|
90
|
+
ensurePrivateFile(DEVICE_ID_PATH);
|
|
91
|
+
} catch {
|
|
92
|
+
// Ignore persistence failures and fall back to the in-memory device id.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getMacOSVersion(): string {
|
|
97
|
+
try {
|
|
98
|
+
return execSync("sw_vers -productVersion", { encoding: "utf8" }).trim();
|
|
99
|
+
} catch {
|
|
100
|
+
return os.release();
|
|
101
|
+
}
|
|
34
102
|
}
|
|
35
103
|
|
|
36
104
|
function getDeviceModel(): string {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
105
|
+
const platform = process.platform;
|
|
106
|
+
const arch = os.machine() || process.arch;
|
|
107
|
+
if (platform === "darwin") {
|
|
108
|
+
const version = getMacOSVersion();
|
|
109
|
+
return version && arch ? `macOS ${version} ${arch}` : `macOS ${arch}`;
|
|
110
|
+
}
|
|
111
|
+
if (platform === "win32") {
|
|
112
|
+
const release = os.release();
|
|
113
|
+
return release && arch ? `Windows ${release} ${arch}` : `Windows ${arch}`;
|
|
114
|
+
}
|
|
115
|
+
const release = os.release();
|
|
116
|
+
return release && arch ? `${platform} ${release} ${arch}` : `${platform} ${arch}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function asciiHeaderValue(value: string, fallback = "unknown"): string {
|
|
120
|
+
const trimmed = value.trim();
|
|
121
|
+
/* oxlint-disable-next-line no-control-regex */
|
|
122
|
+
if (/^[\x00-\x7F]*$/.test(trimmed)) {
|
|
123
|
+
return trimmed;
|
|
124
|
+
}
|
|
125
|
+
/* oxlint-disable-next-line no-control-regex */
|
|
126
|
+
const sanitized = trimmed.replace(/[^\x00-\x7F]/g, "").trim();
|
|
127
|
+
return sanitized || fallback;
|
|
47
128
|
}
|
|
48
129
|
|
|
49
130
|
const DEVICE_MODEL = getDeviceModel();
|
|
50
131
|
let DEVICE_ID: string | null = null;
|
|
51
132
|
|
|
52
133
|
function getStableDeviceId(): string {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
134
|
+
if (DEVICE_ID) {
|
|
135
|
+
return DEVICE_ID;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const persisted = readPersistedDeviceId();
|
|
139
|
+
if (persisted) {
|
|
140
|
+
DEVICE_ID = persisted;
|
|
141
|
+
return DEVICE_ID;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
DEVICE_ID = createDeviceId();
|
|
145
|
+
persistDeviceId(DEVICE_ID);
|
|
146
|
+
return DEVICE_ID;
|
|
57
147
|
}
|
|
58
148
|
|
|
59
149
|
function getCommonHeaders(): Record<string, string> {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
150
|
+
const headers = {
|
|
151
|
+
"User-Agent": KIMI_CLI_USER_AGENT,
|
|
152
|
+
"X-Msh-Platform": KIMI_PLATFORM,
|
|
153
|
+
"X-Msh-Version": KIMI_CLI_VERSION,
|
|
154
|
+
"X-Msh-Device-Name": os.hostname(),
|
|
155
|
+
"X-Msh-Device-Model": DEVICE_MODEL,
|
|
156
|
+
"X-Msh-Os-Version": os.release(),
|
|
157
|
+
"X-Msh-Device-Id": getStableDeviceId(),
|
|
158
|
+
};
|
|
159
|
+
return Object.fromEntries(
|
|
160
|
+
Object.entries(headers).map(([key, value]) => [key, asciiHeaderValue(value)]),
|
|
161
|
+
) as Record<string, string>;
|
|
69
162
|
}
|
|
70
163
|
|
|
71
164
|
// =============================================================================
|
|
@@ -73,127 +166,127 @@ function getCommonHeaders(): Record<string, string> {
|
|
|
73
166
|
// =============================================================================
|
|
74
167
|
|
|
75
168
|
interface DeviceAuthorization {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
169
|
+
user_code: string;
|
|
170
|
+
device_code: string;
|
|
171
|
+
verification_uri: string;
|
|
172
|
+
verification_uri_complete: string;
|
|
173
|
+
expires_in: number;
|
|
174
|
+
interval: number;
|
|
82
175
|
}
|
|
83
176
|
|
|
84
177
|
interface TokenResponse {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
178
|
+
access_token: string;
|
|
179
|
+
refresh_token: string;
|
|
180
|
+
expires_in: number;
|
|
181
|
+
scope: string;
|
|
182
|
+
token_type: string;
|
|
90
183
|
}
|
|
91
184
|
|
|
92
185
|
async function requestDeviceAuthorization(): Promise<DeviceAuthorization> {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
186
|
+
const response = await fetch(`${getOAuthHost()}/api/oauth/device_authorization`, {
|
|
187
|
+
method: "POST",
|
|
188
|
+
headers: {
|
|
189
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
190
|
+
...getCommonHeaders(),
|
|
191
|
+
},
|
|
192
|
+
body: new URLSearchParams({
|
|
193
|
+
client_id: CLIENT_ID,
|
|
194
|
+
}),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
const text = await response.text().catch(() => "");
|
|
199
|
+
throw new Error(`Device authorization failed: ${response.status} ${text}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const data = (await response.json()) as {
|
|
203
|
+
user_code?: string;
|
|
204
|
+
device_code?: string;
|
|
205
|
+
verification_uri?: string;
|
|
206
|
+
verification_uri_complete?: string;
|
|
207
|
+
expires_in?: number;
|
|
208
|
+
interval?: number;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
if (!data.user_code || !data.device_code || !data.verification_uri_complete) {
|
|
212
|
+
throw new Error("Invalid device authorization response");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
user_code: data.user_code,
|
|
217
|
+
device_code: data.device_code,
|
|
218
|
+
verification_uri: data.verification_uri || "",
|
|
219
|
+
verification_uri_complete: data.verification_uri_complete,
|
|
220
|
+
expires_in: data.expires_in || 1800,
|
|
221
|
+
interval: data.interval || 5,
|
|
222
|
+
};
|
|
130
223
|
}
|
|
131
224
|
|
|
132
225
|
async function requestDeviceToken(auth: DeviceAuthorization): Promise<TokenResponse | null> {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
226
|
+
const response = await fetch(`${getOAuthHost()}/api/oauth/token`, {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: {
|
|
229
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
230
|
+
...getCommonHeaders(),
|
|
231
|
+
},
|
|
232
|
+
body: new URLSearchParams({
|
|
233
|
+
client_id: CLIENT_ID,
|
|
234
|
+
device_code: auth.device_code,
|
|
235
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
236
|
+
}),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (response.status === 200) {
|
|
240
|
+
const data = (await response.json()) as TokenResponse;
|
|
241
|
+
if (data.access_token && data.refresh_token) {
|
|
242
|
+
return data;
|
|
243
|
+
}
|
|
244
|
+
throw new Error("Token response missing required fields");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (response.status === 400) {
|
|
248
|
+
const data = (await response.json()) as { error?: string; error_description?: string };
|
|
249
|
+
if (data.error === "authorization_pending") {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
if (data.error === "expired_token") {
|
|
253
|
+
throw new Error("expired_token");
|
|
254
|
+
}
|
|
255
|
+
throw new Error(`Token request failed: ${data.error_description || data.error || "unknown"}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const text = await response.text().catch(() => "");
|
|
259
|
+
throw new Error(`Token request failed: ${response.status} ${text}`);
|
|
167
260
|
}
|
|
168
261
|
|
|
169
262
|
async function refreshAccessToken(refreshToken: string): Promise<TokenResponse> {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
263
|
+
const response = await fetch(`${getOAuthHost()}/api/oauth/token`, {
|
|
264
|
+
method: "POST",
|
|
265
|
+
headers: {
|
|
266
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
267
|
+
...getCommonHeaders(),
|
|
268
|
+
},
|
|
269
|
+
body: new URLSearchParams({
|
|
270
|
+
client_id: CLIENT_ID,
|
|
271
|
+
grant_type: "refresh_token",
|
|
272
|
+
refresh_token: refreshToken,
|
|
273
|
+
}),
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (!response.ok) {
|
|
277
|
+
const text = await response.text().catch(() => "");
|
|
278
|
+
if (response.status === 401 || response.status === 403) {
|
|
279
|
+
throw new Error(`Token refresh unauthorized: ${text}`);
|
|
280
|
+
}
|
|
281
|
+
throw new Error(`Token refresh failed: ${response.status} ${text}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const data = (await response.json()) as TokenResponse;
|
|
285
|
+
if (!data.access_token || !data.refresh_token) {
|
|
286
|
+
throw new Error("Token refresh response missing required fields");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return data;
|
|
197
290
|
}
|
|
198
291
|
|
|
199
292
|
// =============================================================================
|
|
@@ -201,70 +294,70 @@ async function refreshAccessToken(refreshToken: string): Promise<TokenResponse>
|
|
|
201
294
|
// =============================================================================
|
|
202
295
|
|
|
203
296
|
async function loginKimiCode(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
297
|
+
// Keep trying until we get a token (handles expired device codes)
|
|
298
|
+
while (true) {
|
|
299
|
+
const auth = await requestDeviceAuthorization();
|
|
300
|
+
|
|
301
|
+
callbacks.onAuth({
|
|
302
|
+
url: auth.verification_uri_complete,
|
|
303
|
+
instructions: `Please visit the URL to authorize. Your code: ${auth.user_code}`,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const interval = Math.max(auth.interval, 1) * 1000;
|
|
307
|
+
const expiresAt = Date.now() + auth.expires_in * 1000;
|
|
308
|
+
|
|
309
|
+
let token: TokenResponse | null = null;
|
|
310
|
+
let printedWaiting = false;
|
|
311
|
+
|
|
312
|
+
while (Date.now() < expiresAt) {
|
|
313
|
+
try {
|
|
314
|
+
token = await requestDeviceToken(auth);
|
|
315
|
+
if (token) break;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (error instanceof Error && error.message === "expired_token") {
|
|
318
|
+
// Device code expired, restart the flow
|
|
319
|
+
if (callbacks.onProgress) {
|
|
320
|
+
callbacks.onProgress("Device code expired, restarting...");
|
|
321
|
+
}
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!printedWaiting) {
|
|
328
|
+
if (callbacks.onProgress) {
|
|
329
|
+
callbacks.onProgress("Waiting for authorization...");
|
|
330
|
+
}
|
|
331
|
+
printedWaiting = true;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Check for abort
|
|
335
|
+
if (callbacks.signal?.aborted) {
|
|
336
|
+
throw new Error("Authorization aborted");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (token) {
|
|
343
|
+
return {
|
|
344
|
+
access: token.access_token,
|
|
345
|
+
refresh: token.refresh_token,
|
|
346
|
+
expires: Date.now() + token.expires_in * 1000,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// If we get here without a token, the device code expired - loop will retry
|
|
351
|
+
}
|
|
259
352
|
}
|
|
260
353
|
|
|
261
354
|
async function refreshKimiCodeToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
355
|
+
const token = await refreshAccessToken(credentials.refresh);
|
|
356
|
+
return {
|
|
357
|
+
access: token.access_token,
|
|
358
|
+
refresh: token.refresh_token,
|
|
359
|
+
expires: Date.now() + token.expires_in * 1000,
|
|
360
|
+
};
|
|
268
361
|
}
|
|
269
362
|
|
|
270
363
|
// =============================================================================
|
|
@@ -276,78 +369,241 @@ async function refreshKimiCodeToken(credentials: OAuthCredentials): Promise<OAut
|
|
|
276
369
|
|
|
277
370
|
const EMPTY_RESPONSE_PREFIX = "(Empty response:";
|
|
278
371
|
|
|
372
|
+
function mapThinkingLevel(level?: string): { effort: string | null; enabled: boolean } | undefined {
|
|
373
|
+
if (!level) return undefined;
|
|
374
|
+
if (level === "none" || level === "off") return { effort: null, enabled: false };
|
|
375
|
+
if (level === "minimal" || level === "low") return { effort: "low", enabled: true };
|
|
376
|
+
if (level === "medium") return { effort: "medium", enabled: true };
|
|
377
|
+
if (level === "high" || level === "xhigh") return { effort: "high", enabled: true };
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function transformContextFiles(context: Context, apiKey: string): Promise<Context> {
|
|
382
|
+
const transformedMessages = [];
|
|
383
|
+
for (const message of context.messages) {
|
|
384
|
+
if (message.role === "user" && Array.isArray(message.content)) {
|
|
385
|
+
const newContent = [];
|
|
386
|
+
for (const block of message.content) {
|
|
387
|
+
if (block.type === "image") {
|
|
388
|
+
// If the image is extremely large or if it's actually a video masquerading as an image,
|
|
389
|
+
// upload it to Kimi's /v1/files endpoint.
|
|
390
|
+
// For Kimi, images > 5MB or videos should be uploaded. We'll use 5MB threshold.
|
|
391
|
+
const buffer = Buffer.from(block.data, "base64");
|
|
392
|
+
if (buffer.length > 5 * 1024 * 1024 || block.mimeType.startsWith("video/")) {
|
|
393
|
+
const formData = new FormData();
|
|
394
|
+
const filename = block.mimeType.startsWith("video/") ? "upload.mp4" : "upload.png";
|
|
395
|
+
formData.append("file", new Blob([buffer], { type: block.mimeType }), filename);
|
|
396
|
+
formData.append("purpose", block.mimeType.startsWith("video/") ? "video" : "image");
|
|
397
|
+
|
|
398
|
+
const filesBaseUrl = (
|
|
399
|
+
process.env.KIMI_CODE_BASE_URL || "https://api.kimi.com/coding/v1"
|
|
400
|
+
).replace(/\/$/, "");
|
|
401
|
+
const uploadUrl = `${filesBaseUrl}/files`;
|
|
402
|
+
if (process.env.KIMI_CODE_DEBUG === "1") {
|
|
403
|
+
console.log(
|
|
404
|
+
`\n[kimi-coding] Uploading ${filename} to ${uploadUrl} (size: ${(buffer.length / 1024 / 1024).toFixed(2)} MB)...`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
const response = await fetch(uploadUrl, {
|
|
408
|
+
method: "POST",
|
|
409
|
+
headers: {
|
|
410
|
+
Authorization: `Bearer ${apiKey}`,
|
|
411
|
+
...getCommonHeaders(),
|
|
412
|
+
},
|
|
413
|
+
body: formData,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
if (response.ok) {
|
|
417
|
+
const fileObj = (await response.json()) as { id?: string; url?: string };
|
|
418
|
+
const fileUrl = `ms://${fileObj.id}`;
|
|
419
|
+
if (process.env.KIMI_CODE_DEBUG === "1") {
|
|
420
|
+
console.log(`[kimi-coding] Upload success. File URL: ${fileUrl}`);
|
|
421
|
+
}
|
|
422
|
+
// Replace the block with Kimi's specific reference
|
|
423
|
+
if (block.mimeType.startsWith("video/")) {
|
|
424
|
+
newContent.push({
|
|
425
|
+
type: "video_url" as any,
|
|
426
|
+
video_url: { url: fileUrl },
|
|
427
|
+
});
|
|
428
|
+
} else {
|
|
429
|
+
newContent.push({
|
|
430
|
+
type: "image_url" as any,
|
|
431
|
+
image_url: { url: fileUrl },
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
continue;
|
|
435
|
+
} else {
|
|
436
|
+
console.error("Failed to upload file to Kimi API", await response.text());
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
newContent.push(block);
|
|
441
|
+
}
|
|
442
|
+
transformedMessages.push({ ...message, content: newContent });
|
|
443
|
+
} else {
|
|
444
|
+
transformedMessages.push(message);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return { ...context, messages: transformedMessages };
|
|
448
|
+
}
|
|
449
|
+
|
|
279
450
|
function streamSimpleKimi(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
451
|
+
model: Model<"anthropic-messages" | "openai-completions">,
|
|
452
|
+
context: Context,
|
|
453
|
+
options?: SimpleStreamOptions,
|
|
283
454
|
): AssistantMessageEventStream {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
455
|
+
// Pre-process context files: upload large images/videos to Kimi API
|
|
456
|
+
const filtered = new AssistantMessageEventStream();
|
|
457
|
+
const apiKey = options?.apiKey || process.env.KIMI_API_KEY || "";
|
|
458
|
+
|
|
459
|
+
void (async () => {
|
|
460
|
+
let finalContext = context;
|
|
461
|
+
try {
|
|
462
|
+
if (apiKey) {
|
|
463
|
+
finalContext = await transformContextFiles(context, apiKey);
|
|
464
|
+
}
|
|
465
|
+
} catch (e) {
|
|
466
|
+
console.error("Error transforming context files", e);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Intercept the payload to inject Kimi's proprietary prompt_cache_key
|
|
470
|
+
// because Kimi's Anthropic compatibility endpoint requires it alongside cache_control.
|
|
471
|
+
const originalOnPayload = options?.onPayload;
|
|
472
|
+
const patchedOptions: SimpleStreamOptions = {
|
|
473
|
+
...options,
|
|
474
|
+
onPayload: async (payload: any, modelData) => {
|
|
475
|
+
let nextPayload = payload;
|
|
476
|
+
if (originalOnPayload) {
|
|
477
|
+
const res = await originalOnPayload(payload, modelData);
|
|
478
|
+
if (res !== undefined) nextPayload = res;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (nextPayload && typeof nextPayload === "object") {
|
|
482
|
+
// Map unsupported roles: Kimi does not recognize "developer" (OpenAI-specific).
|
|
483
|
+
if (Array.isArray(nextPayload.messages)) {
|
|
484
|
+
nextPayload.messages = nextPayload.messages.map((msg: any) =>
|
|
485
|
+
msg.role === "developer" ? { ...msg, role: "system" } : msg,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Inject prompt_cache_key: allow explicit override, fallback to stable sessionId.
|
|
490
|
+
const cacheKey =
|
|
491
|
+
nextPayload.prompt_cache_key ||
|
|
492
|
+
(options as any)?.prompt_cache_key ||
|
|
493
|
+
options?.sessionId;
|
|
494
|
+
if (cacheKey) {
|
|
495
|
+
nextPayload = { ...nextPayload, prompt_cache_key: cacheKey };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Environment overrides
|
|
499
|
+
const envTemp = process.env.KIMI_MODEL_TEMPERATURE;
|
|
500
|
+
if (envTemp) nextPayload.temperature = parseFloat(envTemp);
|
|
501
|
+
|
|
502
|
+
const envTopP = process.env.KIMI_MODEL_TOP_P;
|
|
503
|
+
if (envTopP) nextPayload.top_p = parseFloat(envTopP);
|
|
504
|
+
|
|
505
|
+
const envMaxTokens = process.env.KIMI_MODEL_MAX_TOKENS;
|
|
506
|
+
if (envMaxTokens) nextPayload.max_tokens = parseInt(envMaxTokens, 10);
|
|
507
|
+
|
|
508
|
+
// Reasoning effort mapping
|
|
509
|
+
const reasoningLevel = (options as any)?.reasoning;
|
|
510
|
+
if (reasoningLevel) {
|
|
511
|
+
const mapped = mapThinkingLevel(reasoningLevel);
|
|
512
|
+
if (mapped) {
|
|
513
|
+
nextPayload.reasoning_effort = mapped.effort;
|
|
514
|
+
nextPayload.extra_body = nextPayload.extra_body || {};
|
|
515
|
+
nextPayload.extra_body.thinking = {
|
|
516
|
+
type: mapped.enabled ? "enabled" : "disabled",
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return nextPayload;
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const upstream =
|
|
526
|
+
model.api === "openai-completions"
|
|
527
|
+
? streamSimpleOpenAICompletions(model as any, finalContext, patchedOptions)
|
|
528
|
+
: streamSimpleAnthropic(model as any, finalContext, patchedOptions);
|
|
529
|
+
|
|
530
|
+
// Buffer text block events so we can suppress the entire block if it
|
|
531
|
+
// turns out to be a Kimi "(Empty response: ...)" wrapper.
|
|
532
|
+
const suppressedIndices = new Set<number>();
|
|
533
|
+
let textBuffer: AssistantMessageEvent[] = [];
|
|
534
|
+
let bufferingIndex: number | null = null;
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
for await (const event of upstream) {
|
|
538
|
+
// Start buffering when a new text block begins
|
|
539
|
+
if (event.type === "text_start") {
|
|
540
|
+
bufferingIndex = event.contentIndex;
|
|
541
|
+
textBuffer = [event];
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Accumulate text deltas while buffering
|
|
546
|
+
if (
|
|
547
|
+
bufferingIndex !== null &&
|
|
548
|
+
"contentIndex" in event &&
|
|
549
|
+
event.contentIndex === bufferingIndex
|
|
550
|
+
) {
|
|
551
|
+
if (event.type === "text_delta") {
|
|
552
|
+
textBuffer.push(event);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (event.type === "text_end") {
|
|
556
|
+
if (event.content.startsWith(EMPTY_RESPONSE_PREFIX)) {
|
|
557
|
+
// Suppress entire text block — discard buffered events.
|
|
558
|
+
// Do NOT splice content array: it is a shared reference
|
|
559
|
+
// into session state, and mutating it shifts subsequent
|
|
560
|
+
// contentIndex values, corrupting the stream.
|
|
561
|
+
suppressedIndices.add(bufferingIndex);
|
|
562
|
+
} else {
|
|
563
|
+
// Legitimate text block — flush buffer + end event
|
|
564
|
+
for (const buffered of textBuffer) filtered.push(buffered);
|
|
565
|
+
filtered.push(event);
|
|
566
|
+
}
|
|
567
|
+
textBuffer = [];
|
|
568
|
+
bufferingIndex = null;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Skip events for already-suppressed indices
|
|
574
|
+
if ("contentIndex" in event && suppressedIndices.has(event.contentIndex)) {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Clean suppressed blocks from the final message
|
|
579
|
+
if (event.type === "done" && suppressedIndices.size > 0) {
|
|
580
|
+
event.message.content = event.message.content.filter(
|
|
581
|
+
(block) =>
|
|
582
|
+
!(
|
|
583
|
+
block.type === "text" &&
|
|
584
|
+
typeof block.text === "string" &&
|
|
585
|
+
block.text.startsWith(EMPTY_RESPONSE_PREFIX)
|
|
586
|
+
),
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
filtered.push(event);
|
|
591
|
+
}
|
|
592
|
+
} catch (err) {
|
|
593
|
+
console.error("[kimi-coding] stream error:", err);
|
|
594
|
+
filtered.push({
|
|
595
|
+
type: "error",
|
|
596
|
+
reason: "error",
|
|
597
|
+
error: {
|
|
598
|
+
content: [],
|
|
599
|
+
stopReason: "error",
|
|
600
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0 },
|
|
601
|
+
},
|
|
602
|
+
} as AssistantMessageEvent & { type: "error" });
|
|
603
|
+
}
|
|
604
|
+
})();
|
|
605
|
+
|
|
606
|
+
return filtered;
|
|
351
607
|
}
|
|
352
608
|
|
|
353
609
|
// =============================================================================
|
|
@@ -355,49 +611,49 @@ function streamSimpleKimi(
|
|
|
355
611
|
// =============================================================================
|
|
356
612
|
|
|
357
613
|
export default function (pi: ExtensionAPI) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
614
|
+
pi.registerProvider("kimi-coding", {
|
|
615
|
+
baseUrl: getBaseUrl(),
|
|
616
|
+
apiKey: "KIMI_API_KEY",
|
|
617
|
+
api: PROTOCOL,
|
|
618
|
+
streamSimple: streamSimpleKimi,
|
|
619
|
+
|
|
620
|
+
headers: getCommonHeaders(),
|
|
621
|
+
|
|
622
|
+
models: [
|
|
623
|
+
{
|
|
624
|
+
id: "kimi-code",
|
|
625
|
+
name: "Kimi Code (powered by kimi-k2.5)",
|
|
626
|
+
reasoning: true,
|
|
627
|
+
input: ["text", "image"],
|
|
628
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
629
|
+
contextWindow: 262144,
|
|
630
|
+
maxTokens: 32000,
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
id: "kimi-k2.5",
|
|
634
|
+
name: "Kimi K2.5",
|
|
635
|
+
reasoning: true,
|
|
636
|
+
input: ["text", "image"],
|
|
637
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
638
|
+
contextWindow: 262144,
|
|
639
|
+
maxTokens: 32000,
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
id: "kimi-k2-thinking-turbo",
|
|
643
|
+
name: "Kimi K2 Thinking Turbo",
|
|
644
|
+
reasoning: true,
|
|
645
|
+
input: ["text"],
|
|
646
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
647
|
+
contextWindow: 262144,
|
|
648
|
+
maxTokens: 32000,
|
|
649
|
+
},
|
|
650
|
+
],
|
|
651
|
+
|
|
652
|
+
oauth: {
|
|
653
|
+
name: "Kimi Code (OAuth)",
|
|
654
|
+
login: loginKimiCode,
|
|
655
|
+
refreshToken: refreshKimiCodeToken,
|
|
656
|
+
getApiKey: (cred) => cred.access,
|
|
657
|
+
},
|
|
658
|
+
});
|
|
403
659
|
}
|
package/package.json
CHANGED
|
@@ -1,30 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-provider-kimi-code",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "pi custom provider extension for Kimi Code models with OAuth device-code login",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "https://github.com/Leechael/pi-provider-kimi-code"
|
|
10
|
-
},
|
|
11
5
|
"keywords": [
|
|
12
|
-
"
|
|
13
|
-
"pi-extension",
|
|
6
|
+
"coding-agent",
|
|
14
7
|
"kimi",
|
|
15
8
|
"kimi-code",
|
|
16
9
|
"llm",
|
|
17
|
-
"
|
|
10
|
+
"pi",
|
|
11
|
+
"pi-extension"
|
|
18
12
|
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/Leechael/pi-provider-kimi-code"
|
|
17
|
+
},
|
|
19
18
|
"files": [
|
|
20
19
|
"index.ts",
|
|
21
20
|
"package.json",
|
|
22
21
|
"README.md"
|
|
23
22
|
],
|
|
23
|
+
"type": "module",
|
|
24
24
|
"scripts": {
|
|
25
25
|
"clean": "echo 'nothing to clean'",
|
|
26
26
|
"build": "echo 'nothing to build'",
|
|
27
|
-
"check": "echo 'nothing to check'"
|
|
27
|
+
"check": "echo 'nothing to check'",
|
|
28
|
+
"lint": "oxlint .",
|
|
29
|
+
"format": "oxfmt . --write",
|
|
30
|
+
"format:check": "oxfmt . --check",
|
|
31
|
+
"pre-commit": "prek run"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@j178/prek": "^0.3.8",
|
|
35
|
+
"oxfmt": "^0.44.0",
|
|
36
|
+
"oxlint": "^1.59.0"
|
|
28
37
|
},
|
|
29
38
|
"pi": {
|
|
30
39
|
"extensions": [
|