pi-provider-kimi-code 0.1.8 → 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 +78 -9
- package/index.ts +547 -352
- package/package.json +20 -11
package/README.md
CHANGED
|
@@ -38,11 +38,11 @@ KIMI_API_KEY=sk-... pi
|
|
|
38
38
|
|
|
39
39
|
## Models
|
|
40
40
|
|
|
41
|
-
| ID
|
|
42
|
-
|
|
43
|
-
| `kimi-code`
|
|
44
|
-
| `kimi-k2.5`
|
|
45
|
-
| `kimi-k2-thinking-turbo`
|
|
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 |
|
|
46
46
|
|
|
47
47
|
Select a model inside pi:
|
|
48
48
|
|
|
@@ -52,16 +52,85 @@ Select a model inside pi:
|
|
|
52
52
|
|
|
53
53
|
## Environment Overrides
|
|
54
54
|
|
|
55
|
-
- `KIMI_CODE_BASE_URL` — override the default API base URL (
|
|
55
|
+
- `KIMI_CODE_BASE_URL` — override the default API base URL (default depends on protocol; see below)
|
|
56
56
|
- `KIMI_CODE_OAUTH_HOST` — override the OAuth host
|
|
57
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.
|
|
58
121
|
|
|
59
122
|
## How It Works
|
|
60
123
|
|
|
61
|
-
- Registers provider `kimi-coding` with
|
|
62
|
-
-
|
|
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`
|
|
63
128
|
- Adds a small `streamSimple` wrapper to suppress Kimi's leaked `(Empty response: ...)` placeholder blocks
|
|
64
|
-
-
|
|
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`
|
|
65
134
|
- Persists a stable device ID at `~/.pi/providers/kimi-coding/device_id`
|
|
66
135
|
- OAuth uses [RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628) device authorization grant against `https://auth.kimi.com`
|
|
67
136
|
- Zero dependencies — types from `@mariozechner/pi-ai` and `@mariozechner/pi-coding-agent` are provided by the pi runtime
|
package/index.ts
CHANGED
|
@@ -9,12 +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";
|
|
13
14
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
15
|
import os from "node:os";
|
|
15
16
|
import { dirname, join } from "node:path";
|
|
16
|
-
import type {
|
|
17
|
-
|
|
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";
|
|
18
30
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
19
31
|
|
|
20
32
|
// =============================================================================
|
|
@@ -23,8 +35,13 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
|
23
35
|
|
|
24
36
|
const CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
|
|
25
37
|
const DEFAULT_OAUTH_HOST = "https://auth.kimi.com";
|
|
26
|
-
const
|
|
27
|
-
|
|
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";
|
|
28
45
|
const KIMI_CLI_USER_AGENT = `KimiCLI/${KIMI_CLI_VERSION}`;
|
|
29
46
|
const KIMI_PLATFORM = "kimi_cli";
|
|
30
47
|
const DEVICE_ID_PATH = join(os.homedir(), ".pi", "providers", "kimi-coding", "device_id");
|
|
@@ -34,99 +51,114 @@ const DEVICE_ID_PATH = join(os.homedir(), ".pi", "providers", "kimi-coding", "de
|
|
|
34
51
|
// =============================================================================
|
|
35
52
|
|
|
36
53
|
function getOAuthHost(): string {
|
|
37
|
-
|
|
38
|
-
|
|
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;
|
|
39
57
|
}
|
|
40
58
|
|
|
41
59
|
function getBaseUrl(): string {
|
|
42
|
-
|
|
43
|
-
|
|
60
|
+
const value = process.env.KIMI_CODE_BASE_URL || DEFAULT_BASE_URL;
|
|
61
|
+
return value.trim() || DEFAULT_BASE_URL;
|
|
44
62
|
}
|
|
45
63
|
|
|
46
64
|
function createDeviceId(): string {
|
|
47
|
-
|
|
65
|
+
return randomBytes(16).toString("hex");
|
|
48
66
|
}
|
|
49
67
|
|
|
50
68
|
function ensurePrivateFile(path: string): void {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
69
|
+
try {
|
|
70
|
+
chmodSync(path, 0o600);
|
|
71
|
+
} catch {
|
|
72
|
+
// Ignore chmod failures on platforms/filesystems that do not support it.
|
|
73
|
+
}
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
function readPersistedDeviceId(): string | null {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
}
|
|
66
84
|
}
|
|
67
85
|
|
|
68
86
|
function persistDeviceId(deviceId: string): void {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
}
|
|
76
102
|
}
|
|
77
103
|
|
|
78
104
|
function getDeviceModel(): string {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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}`;
|
|
89
117
|
}
|
|
90
118
|
|
|
91
119
|
function asciiHeaderValue(value: string, fallback = "unknown"): string {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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;
|
|
98
128
|
}
|
|
99
129
|
|
|
100
130
|
const DEVICE_MODEL = getDeviceModel();
|
|
101
131
|
let DEVICE_ID: string | null = null;
|
|
102
132
|
|
|
103
133
|
function getStableDeviceId(): string {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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;
|
|
117
147
|
}
|
|
118
148
|
|
|
119
149
|
function getCommonHeaders(): Record<string, string> {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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>;
|
|
130
162
|
}
|
|
131
163
|
|
|
132
164
|
// =============================================================================
|
|
@@ -134,127 +166,127 @@ function getCommonHeaders(): Record<string, string> {
|
|
|
134
166
|
// =============================================================================
|
|
135
167
|
|
|
136
168
|
interface DeviceAuthorization {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
169
|
+
user_code: string;
|
|
170
|
+
device_code: string;
|
|
171
|
+
verification_uri: string;
|
|
172
|
+
verification_uri_complete: string;
|
|
173
|
+
expires_in: number;
|
|
174
|
+
interval: number;
|
|
143
175
|
}
|
|
144
176
|
|
|
145
177
|
interface TokenResponse {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
178
|
+
access_token: string;
|
|
179
|
+
refresh_token: string;
|
|
180
|
+
expires_in: number;
|
|
181
|
+
scope: string;
|
|
182
|
+
token_type: string;
|
|
151
183
|
}
|
|
152
184
|
|
|
153
185
|
async function requestDeviceAuthorization(): Promise<DeviceAuthorization> {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
+
};
|
|
191
223
|
}
|
|
192
224
|
|
|
193
225
|
async function requestDeviceToken(auth: DeviceAuthorization): Promise<TokenResponse | null> {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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}`);
|
|
228
260
|
}
|
|
229
261
|
|
|
230
262
|
async function refreshAccessToken(refreshToken: string): Promise<TokenResponse> {
|
|
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
|
-
|
|
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;
|
|
258
290
|
}
|
|
259
291
|
|
|
260
292
|
// =============================================================================
|
|
@@ -262,70 +294,70 @@ async function refreshAccessToken(refreshToken: string): Promise<TokenResponse>
|
|
|
262
294
|
// =============================================================================
|
|
263
295
|
|
|
264
296
|
async function loginKimiCode(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
320
352
|
}
|
|
321
353
|
|
|
322
354
|
async function refreshKimiCodeToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
+
};
|
|
329
361
|
}
|
|
330
362
|
|
|
331
363
|
// =============================================================================
|
|
@@ -337,78 +369,241 @@ async function refreshKimiCodeToken(credentials: OAuthCredentials): Promise<OAut
|
|
|
337
369
|
|
|
338
370
|
const EMPTY_RESPONSE_PREFIX = "(Empty response:";
|
|
339
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
|
+
|
|
340
450
|
function streamSimpleKimi(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
451
|
+
model: Model<"anthropic-messages" | "openai-completions">,
|
|
452
|
+
context: Context,
|
|
453
|
+
options?: SimpleStreamOptions,
|
|
344
454
|
): AssistantMessageEventStream {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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;
|
|
412
607
|
}
|
|
413
608
|
|
|
414
609
|
// =============================================================================
|
|
@@ -416,49 +611,49 @@ function streamSimpleKimi(
|
|
|
416
611
|
// =============================================================================
|
|
417
612
|
|
|
418
613
|
export default function (pi: ExtensionAPI) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
+
});
|
|
464
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": [
|