pi-provider-kimi-code 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -51
- package/index.ts +42 -883
- package/package.json +10 -6
- package/src/constants.ts +48 -0
- package/src/device.ts +164 -0
- package/src/models.ts +129 -0
- package/src/oauth.ts +397 -0
- package/src/payload.ts +446 -0
- package/src/stream.ts +299 -0
package/index.ts
CHANGED
|
@@ -7,907 +7,54 @@
|
|
|
7
7
|
* Usage:
|
|
8
8
|
* pi -e ~/workshop/pi-provider-kimi-code
|
|
9
9
|
* # Then /login kimi-coding, or set KIMI_API_KEY=...
|
|
10
|
+
*
|
|
11
|
+
* Source layout:
|
|
12
|
+
* src/constants.ts — module-level consts + env-driven configuration
|
|
13
|
+
* src/device.ts — device id + kimi-cli-compatible request headers
|
|
14
|
+
* src/oauth.ts — device flow, token refresh, kimi-cli reuse,
|
|
15
|
+
* login/refresh handlers, stream-level auth refresh
|
|
16
|
+
* src/models.ts — /v1/models discovery + extras-merging helpers
|
|
17
|
+
* src/payload.ts — payload pipeline + file upload + transforms
|
|
18
|
+
* src/stream.ts — empty-response filter + streamSimpleKimi orchestrator
|
|
10
19
|
*/
|
|
11
20
|
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
-
import os from "node:os";
|
|
16
|
-
import { dirname, join } from "node:path";
|
|
17
|
-
import type {
|
|
18
|
-
OAuthCredentials,
|
|
19
|
-
OAuthLoginCallbacks,
|
|
20
|
-
AssistantMessageEvent,
|
|
21
|
-
CacheRetention,
|
|
22
|
-
Context,
|
|
23
|
-
Model,
|
|
24
|
-
SimpleStreamOptions,
|
|
25
|
-
ThinkingLevel,
|
|
26
|
-
} from "@mariozechner/pi-ai";
|
|
27
|
-
import {
|
|
28
|
-
streamSimpleAnthropic,
|
|
29
|
-
streamSimpleOpenAICompletions,
|
|
30
|
-
AssistantMessageEventStream,
|
|
31
|
-
} from "@mariozechner/pi-ai";
|
|
32
|
-
import type { ExtensionAPI, OAuthCredential } from "@mariozechner/pi-coding-agent";
|
|
33
|
-
import { AuthStorage } from "@mariozechner/pi-coding-agent";
|
|
34
|
-
|
|
35
|
-
// =============================================================================
|
|
36
|
-
// Constants
|
|
37
|
-
// =============================================================================
|
|
38
|
-
|
|
39
|
-
const CLIENT_ID = "17e5f671-d194-4dfb-9706-5516cb48c098";
|
|
40
|
-
const DEFAULT_OAUTH_HOST = "https://auth.kimi.com";
|
|
41
|
-
const PROTOCOL =
|
|
42
|
-
process.env.KIMI_CODE_PROTOCOL === "openai" ? "openai-completions" : "anthropic-messages";
|
|
43
|
-
const DEFAULT_BASE_URL =
|
|
44
|
-
PROTOCOL === "openai-completions"
|
|
45
|
-
? "https://api.kimi.com/coding/v1"
|
|
46
|
-
: "https://api.kimi.com/coding";
|
|
47
|
-
const KIMI_CLI_VERSION = "1.30.0";
|
|
48
|
-
const KIMI_CLI_USER_AGENT = `KimiCLI/${KIMI_CLI_VERSION}`;
|
|
49
|
-
const KIMI_PLATFORM = "kimi_cli";
|
|
50
|
-
const DEVICE_ID_PATH = join(os.homedir(), ".pi", "providers", "kimi-coding", "device_id");
|
|
51
|
-
|
|
52
|
-
// =============================================================================
|
|
53
|
-
// Device identification
|
|
54
|
-
// =============================================================================
|
|
55
|
-
|
|
56
|
-
function getOAuthHost(): string {
|
|
57
|
-
const value =
|
|
58
|
-
process.env.KIMI_CODE_OAUTH_HOST || process.env.KIMI_OAUTH_HOST || DEFAULT_OAUTH_HOST;
|
|
59
|
-
return value.trim() || DEFAULT_OAUTH_HOST;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function getBaseUrl(): string {
|
|
63
|
-
const value = process.env.KIMI_CODE_BASE_URL || DEFAULT_BASE_URL;
|
|
64
|
-
return value.trim() || DEFAULT_BASE_URL;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function createDeviceId(): string {
|
|
68
|
-
return randomBytes(16).toString("hex");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function ensurePrivateFile(path: string): void {
|
|
72
|
-
try {
|
|
73
|
-
chmodSync(path, 0o600);
|
|
74
|
-
} catch {
|
|
75
|
-
// Ignore chmod failures on platforms/filesystems that do not support it.
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function readPersistedDeviceId(): string | null {
|
|
80
|
-
try {
|
|
81
|
-
if (!existsSync(DEVICE_ID_PATH)) return null;
|
|
82
|
-
const deviceId = readFileSync(DEVICE_ID_PATH, "utf8").trim();
|
|
83
|
-
return deviceId || null;
|
|
84
|
-
} catch {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function persistDeviceId(deviceId: string): void {
|
|
90
|
-
try {
|
|
91
|
-
mkdirSync(dirname(DEVICE_ID_PATH), { recursive: true });
|
|
92
|
-
writeFileSync(DEVICE_ID_PATH, deviceId, "utf8");
|
|
93
|
-
ensurePrivateFile(DEVICE_ID_PATH);
|
|
94
|
-
} catch {
|
|
95
|
-
// Ignore persistence failures and fall back to the in-memory device id.
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function getMacOSVersion(): string {
|
|
100
|
-
try {
|
|
101
|
-
return execSync("sw_vers -productVersion", { encoding: "utf8" }).trim();
|
|
102
|
-
} catch {
|
|
103
|
-
return os.release();
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function getDeviceModel(): string {
|
|
108
|
-
const platform = process.platform;
|
|
109
|
-
const arch = os.machine() || process.arch;
|
|
110
|
-
if (platform === "darwin") {
|
|
111
|
-
const version = getMacOSVersion();
|
|
112
|
-
return version && arch ? `macOS ${version} ${arch}` : `macOS ${arch}`;
|
|
113
|
-
}
|
|
114
|
-
if (platform === "win32") {
|
|
115
|
-
const release = os.release();
|
|
116
|
-
return release && arch ? `Windows ${release} ${arch}` : `Windows ${arch}`;
|
|
117
|
-
}
|
|
118
|
-
const release = os.release();
|
|
119
|
-
return release && arch ? `${platform} ${release} ${arch}` : `${platform} ${arch}`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function asciiHeaderValue(value: string, fallback = "unknown"): string {
|
|
123
|
-
const trimmed = value.trim();
|
|
124
|
-
/* oxlint-disable-next-line no-control-regex */
|
|
125
|
-
if (/^[\x00-\x7F]*$/.test(trimmed)) {
|
|
126
|
-
return trimmed;
|
|
127
|
-
}
|
|
128
|
-
/* oxlint-disable-next-line no-control-regex */
|
|
129
|
-
const sanitized = trimmed.replace(/[^\x00-\x7F]/g, "").trim();
|
|
130
|
-
return sanitized || fallback;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const DEVICE_MODEL = getDeviceModel();
|
|
134
|
-
let DEVICE_ID: string | null = null;
|
|
135
|
-
|
|
136
|
-
function getStableDeviceId(): string {
|
|
137
|
-
if (DEVICE_ID) {
|
|
138
|
-
return DEVICE_ID;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const persisted = readPersistedDeviceId();
|
|
142
|
-
if (persisted) {
|
|
143
|
-
DEVICE_ID = persisted;
|
|
144
|
-
return DEVICE_ID;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
DEVICE_ID = createDeviceId();
|
|
148
|
-
persistDeviceId(DEVICE_ID);
|
|
149
|
-
return DEVICE_ID;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function getCommonHeaders(): Record<string, string> {
|
|
153
|
-
const headers = {
|
|
154
|
-
"User-Agent": KIMI_CLI_USER_AGENT,
|
|
155
|
-
"X-Msh-Platform": KIMI_PLATFORM,
|
|
156
|
-
"X-Msh-Version": KIMI_CLI_VERSION,
|
|
157
|
-
"X-Msh-Device-Name": os.hostname(),
|
|
158
|
-
"X-Msh-Device-Model": DEVICE_MODEL,
|
|
159
|
-
"X-Msh-Os-Version": os.release(),
|
|
160
|
-
"X-Msh-Device-Id": getStableDeviceId(),
|
|
161
|
-
};
|
|
162
|
-
return Object.fromEntries(
|
|
163
|
-
Object.entries(headers).map(([key, value]) => [key, asciiHeaderValue(value)]),
|
|
164
|
-
) as Record<string, string>;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// =============================================================================
|
|
168
|
-
// OAuth Implementation
|
|
169
|
-
// =============================================================================
|
|
170
|
-
|
|
171
|
-
interface DeviceAuthorization {
|
|
172
|
-
user_code: string;
|
|
173
|
-
device_code: string;
|
|
174
|
-
verification_uri: string;
|
|
175
|
-
verification_uri_complete: string;
|
|
176
|
-
expires_in: number;
|
|
177
|
-
interval: number;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
interface TokenResponse {
|
|
181
|
-
access_token: string;
|
|
182
|
-
refresh_token: string;
|
|
183
|
-
expires_in: number;
|
|
184
|
-
scope: string;
|
|
185
|
-
token_type: string;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function requestDeviceAuthorization(): Promise<DeviceAuthorization> {
|
|
189
|
-
const response = await fetch(`${getOAuthHost()}/api/oauth/device_authorization`, {
|
|
190
|
-
method: "POST",
|
|
191
|
-
headers: {
|
|
192
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
193
|
-
...getCommonHeaders(),
|
|
194
|
-
},
|
|
195
|
-
body: new URLSearchParams({
|
|
196
|
-
client_id: CLIENT_ID,
|
|
197
|
-
}),
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
if (!response.ok) {
|
|
201
|
-
const text = await response.text().catch(() => "");
|
|
202
|
-
throw new Error(`Device authorization failed: ${response.status} ${text}`);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const data = (await response.json()) as {
|
|
206
|
-
user_code?: string;
|
|
207
|
-
device_code?: string;
|
|
208
|
-
verification_uri?: string;
|
|
209
|
-
verification_uri_complete?: string;
|
|
210
|
-
expires_in?: number;
|
|
211
|
-
interval?: number;
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
if (!data.user_code || !data.device_code || !data.verification_uri_complete) {
|
|
215
|
-
throw new Error("Invalid device authorization response");
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
user_code: data.user_code,
|
|
220
|
-
device_code: data.device_code,
|
|
221
|
-
verification_uri: data.verification_uri || "",
|
|
222
|
-
verification_uri_complete: data.verification_uri_complete,
|
|
223
|
-
expires_in: data.expires_in || 1800,
|
|
224
|
-
interval: data.interval || 5,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async function requestDeviceToken(auth: DeviceAuthorization): Promise<TokenResponse | null> {
|
|
229
|
-
const response = await fetch(`${getOAuthHost()}/api/oauth/token`, {
|
|
230
|
-
method: "POST",
|
|
231
|
-
headers: {
|
|
232
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
233
|
-
...getCommonHeaders(),
|
|
234
|
-
},
|
|
235
|
-
body: new URLSearchParams({
|
|
236
|
-
client_id: CLIENT_ID,
|
|
237
|
-
device_code: auth.device_code,
|
|
238
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
239
|
-
}),
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
if (response.status === 200) {
|
|
243
|
-
const data = (await response.json()) as TokenResponse;
|
|
244
|
-
if (data.access_token && data.refresh_token) {
|
|
245
|
-
return data;
|
|
246
|
-
}
|
|
247
|
-
throw new Error("Token response missing required fields");
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (response.status === 400) {
|
|
251
|
-
const data = (await response.json()) as { error?: string; error_description?: string };
|
|
252
|
-
if (data.error === "authorization_pending") {
|
|
253
|
-
return null;
|
|
254
|
-
}
|
|
255
|
-
if (data.error === "expired_token") {
|
|
256
|
-
throw new Error("expired_token");
|
|
257
|
-
}
|
|
258
|
-
throw new Error(`Token request failed: ${data.error_description || data.error || "unknown"}`);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const text = await response.text().catch(() => "");
|
|
262
|
-
throw new Error(`Token request failed: ${response.status} ${text}`);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async function refreshAccessToken(refreshToken: string): Promise<TokenResponse> {
|
|
266
|
-
const response = await fetch(`${getOAuthHost()}/api/oauth/token`, {
|
|
267
|
-
method: "POST",
|
|
268
|
-
headers: {
|
|
269
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
270
|
-
...getCommonHeaders(),
|
|
271
|
-
},
|
|
272
|
-
body: new URLSearchParams({
|
|
273
|
-
client_id: CLIENT_ID,
|
|
274
|
-
grant_type: "refresh_token",
|
|
275
|
-
refresh_token: refreshToken,
|
|
276
|
-
}),
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
if (!response.ok) {
|
|
280
|
-
const text = await response.text().catch(() => "");
|
|
281
|
-
if (response.status === 401 || response.status === 403) {
|
|
282
|
-
throw new Error(`Token refresh unauthorized: ${text}`);
|
|
283
|
-
}
|
|
284
|
-
throw new Error(`Token refresh failed: ${response.status} ${text}`);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const data = (await response.json()) as TokenResponse;
|
|
288
|
-
if (!data.access_token || !data.refresh_token) {
|
|
289
|
-
throw new Error("Token refresh response missing required fields");
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return data;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// =============================================================================
|
|
296
|
-
// OAuth login / refresh for extension registration
|
|
297
|
-
// =============================================================================
|
|
298
|
-
|
|
299
|
-
async function loginKimiCode(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
|
300
|
-
// Keep trying until we get a token (handles expired device codes)
|
|
301
|
-
while (true) {
|
|
302
|
-
const auth = await requestDeviceAuthorization();
|
|
303
|
-
|
|
304
|
-
callbacks.onAuth({
|
|
305
|
-
url: auth.verification_uri_complete,
|
|
306
|
-
instructions: `Please visit the URL to authorize. Your code: ${auth.user_code}`,
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
const interval = Math.max(auth.interval, 1) * 1000;
|
|
310
|
-
const expiresAt = Date.now() + auth.expires_in * 1000;
|
|
311
|
-
|
|
312
|
-
let token: TokenResponse | null = null;
|
|
313
|
-
let printedWaiting = false;
|
|
314
|
-
|
|
315
|
-
while (Date.now() < expiresAt) {
|
|
316
|
-
try {
|
|
317
|
-
token = await requestDeviceToken(auth);
|
|
318
|
-
if (token) break;
|
|
319
|
-
} catch (error) {
|
|
320
|
-
if (error instanceof Error && error.message === "expired_token") {
|
|
321
|
-
// Device code expired, restart the flow
|
|
322
|
-
if (callbacks.onProgress) {
|
|
323
|
-
callbacks.onProgress("Device code expired, restarting...");
|
|
324
|
-
}
|
|
325
|
-
break;
|
|
326
|
-
}
|
|
327
|
-
throw error;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (!printedWaiting) {
|
|
331
|
-
if (callbacks.onProgress) {
|
|
332
|
-
callbacks.onProgress("Waiting for authorization...");
|
|
333
|
-
}
|
|
334
|
-
printedWaiting = true;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Check for abort
|
|
338
|
-
if (callbacks.signal?.aborted) {
|
|
339
|
-
throw new Error("Authorization aborted");
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (token) {
|
|
346
|
-
return {
|
|
347
|
-
access: token.access_token,
|
|
348
|
-
refresh: token.refresh_token,
|
|
349
|
-
expires: Date.now() + token.expires_in * 1000,
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// If we get here without a token, the device code expired - loop will retry
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
async function refreshKimiCodeToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
|
358
|
-
const token = await refreshAccessToken(credentials.refresh);
|
|
359
|
-
return {
|
|
360
|
-
access: token.access_token,
|
|
361
|
-
refresh: token.refresh_token,
|
|
362
|
-
expires: Date.now() + token.expires_in * 1000,
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// =============================================================================
|
|
367
|
-
// Payload / stream helpers: types + pure utilities
|
|
368
|
-
// =============================================================================
|
|
369
|
-
|
|
370
|
-
const EMPTY_RESPONSE_PREFIX = "(Empty response:";
|
|
371
|
-
const DEFAULT_KIMI_INLINE_UPLOAD_THRESHOLD_BYTES = 1 * 1024 * 1024;
|
|
372
|
-
|
|
373
|
-
type JsonRecord = Record<string, unknown>;
|
|
374
|
-
type Uploader = (mimeType: string, data: string) => Promise<string | null>;
|
|
375
|
-
|
|
376
|
-
interface KimiEnvOverrides {
|
|
377
|
-
temperature?: number;
|
|
378
|
-
topP?: number;
|
|
379
|
-
maxTokens?: number;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function resolveCacheRetention(value?: CacheRetention): CacheRetention {
|
|
383
|
-
if (value === "none" || value === "short" || value === "long") return value;
|
|
384
|
-
if (process.env.PI_CACHE_RETENTION === "long") return "long";
|
|
385
|
-
return "short";
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
interface KimiPayloadContext {
|
|
389
|
-
api: "anthropic-messages" | "openai-completions";
|
|
390
|
-
upload?: Uploader;
|
|
391
|
-
cacheKey?: string;
|
|
392
|
-
cacheRetention: CacheRetention;
|
|
393
|
-
reasoning?: ThinkingLevel;
|
|
394
|
-
envOverrides: KimiEnvOverrides;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function isRecord(value: unknown): value is JsonRecord {
|
|
398
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function mapThinkingLevel(level?: string): { effort: string | null; enabled: boolean } | undefined {
|
|
402
|
-
if (!level) return undefined;
|
|
403
|
-
if (level === "none" || level === "off") return { effort: null, enabled: false };
|
|
404
|
-
if (level === "minimal" || level === "low") return { effort: "low", enabled: true };
|
|
405
|
-
if (level === "medium") return { effort: "medium", enabled: true };
|
|
406
|
-
if (level === "high" || level === "xhigh") return { effort: "high", enabled: true };
|
|
407
|
-
return undefined;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function parseInlineUploadThreshold(raw: string | undefined): number {
|
|
411
|
-
const parsed = Number.parseInt(raw ?? "", 10);
|
|
412
|
-
return Number.isFinite(parsed) && parsed >= 0
|
|
413
|
-
? parsed
|
|
414
|
-
: DEFAULT_KIMI_INLINE_UPLOAD_THRESHOLD_BYTES;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function deriveFilesBaseUrl(baseUrl: string): string {
|
|
418
|
-
const trimmed = baseUrl.replace(/\/$/, "");
|
|
419
|
-
return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function parseDataUrl(url: string): { mimeType: string; data: string } | null {
|
|
423
|
-
const match = url.match(/^data:([^;,]+);base64,([A-Za-z0-9+/=]+)$/);
|
|
424
|
-
return match ? { mimeType: match[1], data: match[2] } : null;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function getUploadFilename(mimeType: string): string {
|
|
428
|
-
const map: Record<string, string> = {
|
|
429
|
-
"image/jpeg": "upload.jpg",
|
|
430
|
-
"image/png": "upload.png",
|
|
431
|
-
"image/gif": "upload.gif",
|
|
432
|
-
"image/webp": "upload.webp",
|
|
433
|
-
"video/mp4": "upload.mp4",
|
|
434
|
-
"video/quicktime": "upload.mov",
|
|
435
|
-
};
|
|
436
|
-
return map[mimeType] ?? (mimeType.startsWith("video/") ? "upload.mp4" : "upload.bin");
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function readEnvOverrides(): KimiEnvOverrides {
|
|
440
|
-
const out: KimiEnvOverrides = {};
|
|
441
|
-
const temp = process.env.KIMI_MODEL_TEMPERATURE;
|
|
442
|
-
if (temp) out.temperature = parseFloat(temp);
|
|
443
|
-
const topP = process.env.KIMI_MODEL_TOP_P;
|
|
444
|
-
if (topP) out.topP = parseFloat(topP);
|
|
445
|
-
const maxTokens = process.env.KIMI_MODEL_MAX_TOKENS;
|
|
446
|
-
if (maxTokens) out.maxTokens = parseInt(maxTokens, 10);
|
|
447
|
-
return out;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// =============================================================================
|
|
451
|
-
// File upload (I/O edge)
|
|
452
|
-
// =============================================================================
|
|
453
|
-
|
|
454
|
-
async function uploadKimiFile(
|
|
455
|
-
apiKey: string,
|
|
456
|
-
mimeType: string,
|
|
457
|
-
data: string,
|
|
458
|
-
): Promise<string | null> {
|
|
459
|
-
const buffer = Buffer.from(data, "base64");
|
|
460
|
-
const isVideo = mimeType.startsWith("video/");
|
|
461
|
-
const threshold = parseInlineUploadThreshold(process.env.KIMI_CODE_UPLOAD_THRESHOLD_BYTES);
|
|
462
|
-
if (!isVideo && buffer.length <= threshold) return null;
|
|
463
|
-
|
|
464
|
-
const filename = getUploadFilename(mimeType);
|
|
465
|
-
const formData = new FormData();
|
|
466
|
-
formData.append("file", new Blob([buffer], { type: mimeType }), filename);
|
|
467
|
-
formData.append("purpose", isVideo ? "video" : "image");
|
|
468
|
-
|
|
469
|
-
const baseUrl = process.env.KIMI_CODE_BASE_URL || DEFAULT_BASE_URL;
|
|
470
|
-
const uploadUrl = `${deriveFilesBaseUrl(baseUrl)}/files`;
|
|
471
|
-
const debug = process.env.KIMI_CODE_DEBUG === "1";
|
|
472
|
-
if (debug) {
|
|
473
|
-
console.log(
|
|
474
|
-
`\n[kimi-coding] Uploading ${filename} to ${uploadUrl} (${(buffer.length / 1024 / 1024).toFixed(2)} MB)`,
|
|
475
|
-
);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
try {
|
|
479
|
-
const response = await fetch(uploadUrl, {
|
|
480
|
-
method: "POST",
|
|
481
|
-
headers: { Authorization: `Bearer ${apiKey}`, ...getCommonHeaders() },
|
|
482
|
-
body: formData,
|
|
483
|
-
});
|
|
484
|
-
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
|
|
485
|
-
const fileObj = (await response.json()) as { id?: string };
|
|
486
|
-
if (!fileObj.id) throw new Error("missing file id");
|
|
487
|
-
const fileUrl = `ms://${fileObj.id}`;
|
|
488
|
-
if (debug) console.log(`[kimi-coding] Upload success: ${fileUrl}`);
|
|
489
|
-
return fileUrl;
|
|
490
|
-
} catch (err) {
|
|
491
|
-
console.error("[kimi-coding] Upload failed:", err);
|
|
492
|
-
return null;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// =============================================================================
|
|
497
|
-
// Payload file transformers (pure given an Uploader)
|
|
498
|
-
// =============================================================================
|
|
499
|
-
// These walk the provider-specific payload shape and replace inline base64
|
|
500
|
-
// image/video blocks with ms:// references returned by the injected uploader.
|
|
501
|
-
// They take an Uploader rather than an apiKey so they can be unit-tested with
|
|
502
|
-
// a fake uploader; all network I/O stays behind that boundary.
|
|
503
|
-
|
|
504
|
-
async function transformOpenAIPayloadFiles(payload: JsonRecord, upload: Uploader): Promise<void> {
|
|
505
|
-
if (!Array.isArray(payload.messages)) return;
|
|
506
|
-
const cache = new Map<string, string>();
|
|
507
|
-
|
|
508
|
-
for (const message of payload.messages) {
|
|
509
|
-
if (!isRecord(message) || !Array.isArray(message.content)) continue;
|
|
510
|
-
|
|
511
|
-
for (const block of message.content) {
|
|
512
|
-
if (!isRecord(block)) continue;
|
|
513
|
-
const key =
|
|
514
|
-
block.type === "image_url" ? "image_url" : block.type === "video_url" ? "video_url" : null;
|
|
515
|
-
if (!key) continue;
|
|
516
|
-
|
|
517
|
-
const field = block[key];
|
|
518
|
-
const urlValue =
|
|
519
|
-
typeof field === "string"
|
|
520
|
-
? field
|
|
521
|
-
: isRecord(field) && typeof field.url === "string"
|
|
522
|
-
? field.url
|
|
523
|
-
: null;
|
|
524
|
-
if (!urlValue || urlValue.startsWith("ms://")) continue;
|
|
525
|
-
|
|
526
|
-
const parsed = parseDataUrl(urlValue);
|
|
527
|
-
if (!parsed) continue;
|
|
528
|
-
|
|
529
|
-
const uploaded = cache.get(urlValue) ?? (await upload(parsed.mimeType, parsed.data));
|
|
530
|
-
if (!uploaded) continue;
|
|
531
|
-
cache.set(urlValue, uploaded);
|
|
532
|
-
|
|
533
|
-
block[key] =
|
|
534
|
-
typeof field === "string" ? uploaded : { ...(field as JsonRecord), url: uploaded };
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
async function transformAnthropicPayloadFiles(
|
|
540
|
-
payload: JsonRecord,
|
|
541
|
-
upload: Uploader,
|
|
542
|
-
): Promise<void> {
|
|
543
|
-
if (!Array.isArray(payload.messages)) return;
|
|
544
|
-
const cache = new Map<string, string>();
|
|
545
|
-
|
|
546
|
-
const transformImageBlock = async (block: unknown): Promise<unknown> => {
|
|
547
|
-
if (!isRecord(block) || block.type !== "image") return block;
|
|
548
|
-
const source = block.source;
|
|
549
|
-
if (!isRecord(source) || source.type !== "base64") return block;
|
|
550
|
-
const mediaType = source.media_type;
|
|
551
|
-
const data = source.data;
|
|
552
|
-
if (typeof mediaType !== "string" || typeof data !== "string") return block;
|
|
553
|
-
|
|
554
|
-
const cacheKey = `${mediaType}:${data}`;
|
|
555
|
-
const uploaded = cache.get(cacheKey) ?? (await upload(mediaType, data));
|
|
556
|
-
if (!uploaded) return block;
|
|
557
|
-
cache.set(cacheKey, uploaded);
|
|
558
|
-
|
|
559
|
-
const next: JsonRecord = { type: "image", source: { type: "url", url: uploaded } };
|
|
560
|
-
if (block.cache_control !== undefined) next.cache_control = block.cache_control;
|
|
561
|
-
return next;
|
|
562
|
-
};
|
|
563
|
-
|
|
564
|
-
for (const message of payload.messages) {
|
|
565
|
-
if (!isRecord(message) || !Array.isArray(message.content)) continue;
|
|
566
|
-
|
|
567
|
-
for (let i = 0; i < message.content.length; i++) {
|
|
568
|
-
const block = message.content[i];
|
|
569
|
-
if (isRecord(block) && block.type === "tool_result" && Array.isArray(block.content)) {
|
|
570
|
-
for (let j = 0; j < block.content.length; j++) {
|
|
571
|
-
block.content[j] = await transformImageBlock(block.content[j]);
|
|
572
|
-
}
|
|
573
|
-
continue;
|
|
574
|
-
}
|
|
575
|
-
message.content[i] = await transformImageBlock(block);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// =============================================================================
|
|
581
|
-
// Payload mutation pipeline
|
|
582
|
-
// =============================================================================
|
|
583
|
-
// Applies all Kimi-specific mutations to a provider payload in place.
|
|
584
|
-
// Pure given its context: no process.env / fs / network access of its own —
|
|
585
|
-
// every side effect enters via ctx.upload or pre-read values in ctx.
|
|
586
|
-
// This makes the five steps below testable with fixture payloads.
|
|
21
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
22
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
587
23
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
await transformOpenAIPayloadFiles(payload, ctx.upload);
|
|
603
|
-
} else if (ctx.api === "anthropic-messages") {
|
|
604
|
-
await transformAnthropicPayloadFiles(payload, ctx.upload);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// 3. prompt_cache_key injection. Respect any key already on the payload,
|
|
609
|
-
// otherwise fall back to the caller-provided cacheKey (sessionId or
|
|
610
|
-
// explicit options.prompt_cache_key override). Skipped entirely when
|
|
611
|
-
// cacheRetention is "none" (via options.cacheRetention or
|
|
612
|
-
// PI_CACHE_RETENTION) so callers can truly disable caching — otherwise
|
|
613
|
-
// Kimi's native session cache would still fire even if pi-ai's
|
|
614
|
-
// Anthropic-style cache_control markers are omitted.
|
|
615
|
-
if (ctx.cacheRetention !== "none") {
|
|
616
|
-
const existing = payload.prompt_cache_key;
|
|
617
|
-
const resolved = (typeof existing === "string" && existing) || ctx.cacheKey;
|
|
618
|
-
if (resolved) payload.prompt_cache_key = resolved;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// 4. Env-level hyperparameter overrides (pre-parsed into numbers by caller).
|
|
622
|
-
const { temperature, topP, maxTokens } = ctx.envOverrides;
|
|
623
|
-
if (temperature !== undefined) payload.temperature = temperature;
|
|
624
|
-
if (topP !== undefined) payload.top_p = topP;
|
|
625
|
-
if (maxTokens !== undefined) payload.max_tokens = maxTokens;
|
|
626
|
-
|
|
627
|
-
// 5. Reasoning effort mapping.
|
|
628
|
-
if (ctx.reasoning) {
|
|
629
|
-
const mapped = mapThinkingLevel(ctx.reasoning);
|
|
630
|
-
if (mapped) {
|
|
631
|
-
payload.reasoning_effort = mapped.effort;
|
|
632
|
-
const extraBody = isRecord(payload.extra_body) ? payload.extra_body : {};
|
|
633
|
-
extraBody.thinking = { type: mapped.enabled ? "enabled" : "disabled" };
|
|
634
|
-
payload.extra_body = extraBody;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// =============================================================================
|
|
640
|
-
// Event stream filter: suppress Kimi "(Empty response: ...)" text blocks
|
|
641
|
-
// =============================================================================
|
|
642
|
-
// The Kimi API wraps thinking-only responses (no text content) into a text
|
|
643
|
-
// block like: (Empty response: {'content': [{'type': 'thinking', ...}]}).
|
|
644
|
-
// This leaks internal state to the user. We buffer text_start/text_delta
|
|
645
|
-
// events and drop the whole block if text_end starts with the marker.
|
|
646
|
-
// Pure async generator — no closure dependencies, testable with synthetic
|
|
647
|
-
// event arrays.
|
|
648
|
-
|
|
649
|
-
async function* filterEmptyResponseStream(
|
|
650
|
-
upstream: AsyncIterable<AssistantMessageEvent>,
|
|
651
|
-
): AsyncIterable<AssistantMessageEvent> {
|
|
652
|
-
const suppressedIndices = new Set<number>();
|
|
653
|
-
let textBuffer: AssistantMessageEvent[] = [];
|
|
654
|
-
let bufferingIndex: number | null = null;
|
|
655
|
-
|
|
656
|
-
for await (const event of upstream) {
|
|
657
|
-
// Start buffering when a new text block begins.
|
|
658
|
-
if (event.type === "text_start") {
|
|
659
|
-
bufferingIndex = event.contentIndex;
|
|
660
|
-
textBuffer = [event];
|
|
661
|
-
continue;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Accumulate text deltas + detect the empty-response marker on text_end.
|
|
665
|
-
if (
|
|
666
|
-
bufferingIndex !== null &&
|
|
667
|
-
"contentIndex" in event &&
|
|
668
|
-
event.contentIndex === bufferingIndex
|
|
669
|
-
) {
|
|
670
|
-
if (event.type === "text_delta") {
|
|
671
|
-
textBuffer.push(event);
|
|
672
|
-
continue;
|
|
673
|
-
}
|
|
674
|
-
if (event.type === "text_end") {
|
|
675
|
-
if (event.content.startsWith(EMPTY_RESPONSE_PREFIX)) {
|
|
676
|
-
// Suppress entire text block. Do NOT splice the message content
|
|
677
|
-
// array: it is a shared reference into session state, and mutating
|
|
678
|
-
// it would shift subsequent contentIndex values, corrupting the
|
|
679
|
-
// stream.
|
|
680
|
-
suppressedIndices.add(bufferingIndex);
|
|
681
|
-
} else {
|
|
682
|
-
// Legitimate text block — flush buffered events + end event.
|
|
683
|
-
for (const buffered of textBuffer) yield buffered;
|
|
684
|
-
yield event;
|
|
685
|
-
}
|
|
686
|
-
textBuffer = [];
|
|
687
|
-
bufferingIndex = null;
|
|
688
|
-
continue;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// Skip any event targeting an already-suppressed content index.
|
|
693
|
-
if ("contentIndex" in event && suppressedIndices.has(event.contentIndex)) {
|
|
694
|
-
continue;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// Clean suppressed blocks out of the final message.
|
|
698
|
-
if (event.type === "done" && suppressedIndices.size > 0) {
|
|
699
|
-
event.message.content = event.message.content.filter(
|
|
700
|
-
(block) =>
|
|
701
|
-
!(
|
|
702
|
-
block.type === "text" &&
|
|
703
|
-
typeof block.text === "string" &&
|
|
704
|
-
block.text.startsWith(EMPTY_RESPONSE_PREFIX)
|
|
705
|
-
),
|
|
706
|
-
);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
yield event;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
// =============================================================================
|
|
714
|
-
// Auth refresh: recover from server-side token invalidation
|
|
715
|
-
// =============================================================================
|
|
716
|
-
// pi-coding-agent only refreshes an OAuth token when the locally cached
|
|
717
|
-
// `expires` is in the past. If the server rotates/revokes the access token
|
|
718
|
-
// before that (common with short-lived session tokens), every request keeps
|
|
719
|
-
// returning 401. We detect that situation by inspecting the first event of
|
|
720
|
-
// the upstream stream, force a refresh through AuthStorage (which persists
|
|
721
|
-
// the new credentials under a file lock), and retry once.
|
|
722
|
-
|
|
723
|
-
const PROVIDER_ID = "kimi-coding";
|
|
724
|
-
|
|
725
|
-
async function refreshKimiAuthToken(currentKey: string): Promise<string | null> {
|
|
726
|
-
try {
|
|
727
|
-
const storage = AuthStorage.create();
|
|
728
|
-
const cred = storage.get(PROVIDER_ID);
|
|
729
|
-
if (!cred || cred.type !== "oauth") {
|
|
730
|
-
console.error(
|
|
731
|
-
`[kimi-coding] auth refresh skipped: no OAuth credentials for ${PROVIDER_ID} on disk`,
|
|
732
|
-
);
|
|
733
|
-
return null;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
// If disk already has a different valid token (e.g., another process or a
|
|
737
|
-
// previous retry refreshed it while the caller's in-memory cache went
|
|
738
|
-
// stale), reuse it without hitting the OAuth endpoint.
|
|
739
|
-
if (cred.access !== currentKey && Date.now() < cred.expires) {
|
|
740
|
-
console.error("[kimi-coding] auth refresh: reusing newer on-disk token");
|
|
741
|
-
return cred.access;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
console.error("[kimi-coding] auth refresh: requesting new access token");
|
|
745
|
-
const refreshed = await refreshAccessToken(cred.refresh);
|
|
746
|
-
const newCred: OAuthCredential = {
|
|
747
|
-
type: "oauth",
|
|
748
|
-
access: refreshed.access_token,
|
|
749
|
-
refresh: refreshed.refresh_token,
|
|
750
|
-
expires: Date.now() + refreshed.expires_in * 1000,
|
|
751
|
-
};
|
|
752
|
-
storage.set(PROVIDER_ID, newCred);
|
|
753
|
-
console.error("[kimi-coding] auth refresh: new token persisted");
|
|
754
|
-
return newCred.access;
|
|
755
|
-
} catch (err) {
|
|
756
|
-
console.error("[kimi-coding] auth refresh failed:", err);
|
|
757
|
-
return null;
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
// =============================================================================
|
|
762
|
-
// Stream wrapper: orchestrates payload mutation + event filter
|
|
763
|
-
// =============================================================================
|
|
764
|
-
// Reads every side-effect source (process.env, options, apiKey) at the top
|
|
765
|
-
// and hands a plain KimiPayloadContext to applyKimiPayloadMutations. The only
|
|
766
|
-
// thing this function itself "does" is wire SDK streaming + filter + error
|
|
767
|
-
// fallback; the actual logic lives in the pure units above.
|
|
768
|
-
|
|
769
|
-
function streamSimpleKimi(
|
|
770
|
-
model: Model<"anthropic-messages" | "openai-completions">,
|
|
771
|
-
context: Context,
|
|
772
|
-
options?: SimpleStreamOptions,
|
|
773
|
-
): AssistantMessageEventStream {
|
|
774
|
-
const filtered = new AssistantMessageEventStream();
|
|
775
|
-
const initialKey = options?.apiKey || process.env.KIMI_API_KEY || "";
|
|
776
|
-
|
|
777
|
-
const cacheKeyOverride = (
|
|
778
|
-
options as (SimpleStreamOptions & { prompt_cache_key?: unknown }) | undefined
|
|
779
|
-
)?.prompt_cache_key;
|
|
780
|
-
const cacheKey = (typeof cacheKeyOverride === "string" && cacheKeyOverride) || options?.sessionId;
|
|
781
|
-
const cacheRetention = resolveCacheRetention(options?.cacheRetention);
|
|
782
|
-
const envOverrides = readEnvOverrides();
|
|
783
|
-
const originalOnPayload = options?.onPayload;
|
|
784
|
-
|
|
785
|
-
const buildPatchedOptions = (apiKey: string): SimpleStreamOptions => {
|
|
786
|
-
const upload: Uploader | undefined = apiKey
|
|
787
|
-
? (mimeType, data) => uploadKimiFile(apiKey, mimeType, data)
|
|
788
|
-
: undefined;
|
|
789
|
-
return {
|
|
790
|
-
...options,
|
|
791
|
-
apiKey,
|
|
792
|
-
onPayload: async (payload, modelData) => {
|
|
793
|
-
let nextPayload: unknown = payload;
|
|
794
|
-
|
|
795
|
-
if (isRecord(nextPayload)) {
|
|
796
|
-
await applyKimiPayloadMutations(nextPayload, {
|
|
797
|
-
api: model.api,
|
|
798
|
-
upload,
|
|
799
|
-
cacheKey,
|
|
800
|
-
cacheRetention,
|
|
801
|
-
reasoning: options?.reasoning,
|
|
802
|
-
envOverrides,
|
|
803
|
-
});
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
if (originalOnPayload) {
|
|
807
|
-
const res = await originalOnPayload(nextPayload, modelData);
|
|
808
|
-
if (res !== undefined) nextPayload = res;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
return nextPayload;
|
|
812
|
-
},
|
|
813
|
-
};
|
|
814
|
-
};
|
|
815
|
-
|
|
816
|
-
void (async () => {
|
|
817
|
-
let attempt = 0;
|
|
818
|
-
let currentKey = initialKey;
|
|
819
|
-
|
|
820
|
-
while (true) {
|
|
821
|
-
const patchedOptions = buildPatchedOptions(currentKey);
|
|
822
|
-
const upstream =
|
|
823
|
-
model.api === "openai-completions"
|
|
824
|
-
? streamSimpleOpenAICompletions(
|
|
825
|
-
model as Model<"openai-completions">,
|
|
826
|
-
context,
|
|
827
|
-
patchedOptions,
|
|
828
|
-
)
|
|
829
|
-
: streamSimpleAnthropic(model as Model<"anthropic-messages">, context, patchedOptions);
|
|
830
|
-
|
|
831
|
-
let pushedAny = false;
|
|
832
|
-
let shouldRetry = false;
|
|
833
|
-
|
|
834
|
-
try {
|
|
835
|
-
for await (const event of filterEmptyResponseStream(upstream)) {
|
|
836
|
-
// If the upstream terminates with an error before we've emitted
|
|
837
|
-
// anything downstream, speculatively try an OAuth refresh and
|
|
838
|
-
// retry once. This handles the common case of a server-side token
|
|
839
|
-
// invalidation before the local `expires` lapses, without having
|
|
840
|
-
// to pattern-match every possible error-message format.
|
|
841
|
-
//
|
|
842
|
-
// Non-auth errors (overflow, network, rate-limit, etc.) still
|
|
843
|
-
// trigger one wasted refresh, but the retried request fails the
|
|
844
|
-
// same way and we forward it — pi-coding-agent's own recovery
|
|
845
|
-
// paths (compaction, retry) take over from there.
|
|
846
|
-
if (!pushedAny && attempt === 0 && event.type === "error") {
|
|
847
|
-
console.error(
|
|
848
|
-
`[kimi-coding] upstream error on first event, attempting refresh: ${event.error?.errorMessage?.slice(0, 200)}`,
|
|
849
|
-
);
|
|
850
|
-
const refreshed = await refreshKimiAuthToken(currentKey);
|
|
851
|
-
if (refreshed && refreshed !== currentKey) {
|
|
852
|
-
console.error("[kimi-coding] retrying stream with refreshed token");
|
|
853
|
-
currentKey = refreshed;
|
|
854
|
-
shouldRetry = true;
|
|
855
|
-
break;
|
|
856
|
-
}
|
|
857
|
-
console.error(
|
|
858
|
-
"[kimi-coding] refresh did not yield a new token, forwarding original error",
|
|
859
|
-
);
|
|
860
|
-
}
|
|
861
|
-
filtered.push(event);
|
|
862
|
-
pushedAny = true;
|
|
863
|
-
}
|
|
864
|
-
} catch (err) {
|
|
865
|
-
console.error("[kimi-coding] stream error:", err);
|
|
866
|
-
filtered.push({
|
|
867
|
-
type: "error",
|
|
868
|
-
reason: "error",
|
|
869
|
-
error: {
|
|
870
|
-
content: [],
|
|
871
|
-
stopReason: "error",
|
|
872
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0 },
|
|
873
|
-
},
|
|
874
|
-
} as AssistantMessageEvent & { type: "error" });
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
if (shouldRetry) {
|
|
878
|
-
attempt++;
|
|
879
|
-
continue;
|
|
880
|
-
}
|
|
881
|
-
break;
|
|
882
|
-
}
|
|
883
|
-
})();
|
|
884
|
-
|
|
885
|
-
return filtered;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// =============================================================================
|
|
889
|
-
// Extension Entry Point
|
|
890
|
-
// =============================================================================
|
|
24
|
+
import {
|
|
25
|
+
DEFAULT_KIMI_MODEL_INPUT,
|
|
26
|
+
KIMI_API_TYPE,
|
|
27
|
+
PROVIDER_ID,
|
|
28
|
+
getBaseUrl,
|
|
29
|
+
} from "./src/constants.ts";
|
|
30
|
+
import { getCommonHeaders } from "./src/device.ts";
|
|
31
|
+
import {
|
|
32
|
+
type KimiOAuthCredentials,
|
|
33
|
+
applyKimiEnvOverridesToModel,
|
|
34
|
+
applyKimiOAuthExtrasToModel,
|
|
35
|
+
} from "./src/models.ts";
|
|
36
|
+
import { loginKimiCode, refreshKimiCodeToken } from "./src/oauth.ts";
|
|
37
|
+
import { streamSimpleKimi } from "./src/stream.ts";
|
|
891
38
|
|
|
892
39
|
export default function (pi: ExtensionAPI) {
|
|
893
|
-
pi.registerProvider(
|
|
40
|
+
pi.registerProvider(PROVIDER_ID, {
|
|
894
41
|
baseUrl: getBaseUrl(),
|
|
895
42
|
apiKey: "KIMI_API_KEY",
|
|
896
|
-
api:
|
|
43
|
+
api: KIMI_API_TYPE,
|
|
897
44
|
streamSimple: streamSimpleKimi,
|
|
898
45
|
|
|
899
46
|
headers: getCommonHeaders(),
|
|
900
47
|
|
|
901
48
|
models: [
|
|
902
|
-
{
|
|
49
|
+
applyKimiEnvOverridesToModel({
|
|
903
50
|
id: "kimi-for-coding",
|
|
904
51
|
name: "Kimi for Coding",
|
|
905
52
|
reasoning: true,
|
|
906
|
-
input: ["text"
|
|
53
|
+
input: [...DEFAULT_KIMI_MODEL_INPUT] as unknown as ("text" | "image")[],
|
|
907
54
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
908
55
|
contextWindow: 262144,
|
|
909
56
|
maxTokens: 32000,
|
|
910
|
-
},
|
|
57
|
+
} as Model<Api>),
|
|
911
58
|
],
|
|
912
59
|
|
|
913
60
|
oauth: {
|
|
@@ -915,6 +62,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
915
62
|
login: loginKimiCode,
|
|
916
63
|
refreshToken: refreshKimiCodeToken,
|
|
917
64
|
getApiKey: (cred) => cred.access,
|
|
65
|
+
// Reflect server-side model identity on the registered model after login
|
|
66
|
+
// / refresh. We never rewrite the model id (pi-side `/model` selections
|
|
67
|
+
// and persisted sessions reference it); only the human-facing name, the
|
|
68
|
+
// context window, and an out-of-band `wireModelId` carried into the
|
|
69
|
+
// request payload by streamSimpleKimi.
|
|
70
|
+
modifyModels: (models, cred) => {
|
|
71
|
+
const extras = cred as KimiOAuthCredentials;
|
|
72
|
+
return models.map((model) => {
|
|
73
|
+
if (model.id !== "kimi-for-coding") return model;
|
|
74
|
+
return applyKimiEnvOverridesToModel(applyKimiOAuthExtrasToModel(model, extras));
|
|
75
|
+
});
|
|
76
|
+
},
|
|
918
77
|
},
|
|
919
78
|
});
|
|
920
79
|
}
|