pi-provider-kimi-code 0.6.6 → 0.6.7
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/package.json +8 -4
- package/src/constants.ts +1 -1
- package/src/oauth.ts +91 -12
- package/src/stream.ts +53 -7
- package/src/tools/common.ts +7 -5
- package/src/usage.ts +3 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-provider-kimi-code",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"description": "Use Kimi Code in Pi without losing the Kimi-specific bits",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-provider",
|
|
@@ -49,12 +49,16 @@
|
|
|
49
49
|
"format:check": "oxfmt . --check",
|
|
50
50
|
"pre-commit": "prek run"
|
|
51
51
|
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"proper-lockfile": "^4.1.2"
|
|
54
|
+
},
|
|
52
55
|
"devDependencies": {
|
|
53
|
-
"@earendil-works/pi-ai": "^0.
|
|
54
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
55
|
-
"@earendil-works/pi-tui": "^0.
|
|
56
|
+
"@earendil-works/pi-ai": "^0.80.9",
|
|
57
|
+
"@earendil-works/pi-coding-agent": "^0.80.9",
|
|
58
|
+
"@earendil-works/pi-tui": "^0.80.9",
|
|
56
59
|
"@j178/prek": "^0.4.4",
|
|
57
60
|
"@types/node": "^24.0.0",
|
|
61
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
58
62
|
"oxfmt": "^0.44.0",
|
|
59
63
|
"oxlint": "^1.59.0",
|
|
60
64
|
"typescript": "^5.9.0"
|
package/src/constants.ts
CHANGED
|
@@ -41,7 +41,7 @@ export function getDefaultBaseUrl(protocol: KimiWireProtocol): string {
|
|
|
41
41
|
|
|
42
42
|
export const DEFAULT_BASE_URL = getDefaultBaseUrl(ENV_KIMI_CODE_PROTOCOL);
|
|
43
43
|
|
|
44
|
-
export const PROVIDER_VERSION = "0.6.
|
|
44
|
+
export const PROVIDER_VERSION = "0.6.7";
|
|
45
45
|
|
|
46
46
|
// Upstream kimi-code CLI version — used in User-Agent and X-Msh-Version
|
|
47
47
|
// headers to match the official client's identity. Update this when
|
package/src/oauth.ts
CHANGED
|
@@ -2,13 +2,94 @@
|
|
|
2
2
|
// reuse, the login / refresh handlers wired into pi's OAuth interface, and
|
|
3
3
|
// the stream-level auth refresh used by the streaming handler.
|
|
4
4
|
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
|
|
9
|
+
import { lock as acquireFileLock } from "proper-lockfile";
|
|
10
|
+
|
|
9
11
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
+
import * as piAgent from "@earendil-works/pi-coding-agent";
|
|
13
|
+
|
|
14
|
+
// Structural mirrors of pi's stored credential shapes. The exported names
|
|
15
|
+
// moved between pi versions (pi-coding-agent <=0.79 vs pi-ai >=0.80.8), so
|
|
16
|
+
// these are defined locally to type-check against either surface.
|
|
17
|
+
type StoredCredential = { type: string } & Record<string, unknown>;
|
|
18
|
+
|
|
19
|
+
export interface StoredOAuthCredential {
|
|
20
|
+
type: "oauth";
|
|
21
|
+
access: string;
|
|
22
|
+
refresh: string;
|
|
23
|
+
expires: number;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// pi renamed its auth storage surface in 0.80.8: the exported AuthStorage
|
|
28
|
+
// class (sync get/set under a file lock) was replaced by readStoredCredential
|
|
29
|
+
// for one-off reads, with no exported write path. Detect whichever the host
|
|
30
|
+
// pi provides so the extension keeps loading on both sides of the rename.
|
|
31
|
+
const piAgentRuntime = piAgent as unknown as {
|
|
32
|
+
getAgentDir(): string;
|
|
33
|
+
readStoredCredential?(providerId: string): StoredCredential | undefined;
|
|
34
|
+
AuthStorage?: {
|
|
35
|
+
create(): {
|
|
36
|
+
get(provider: string): StoredCredential | undefined;
|
|
37
|
+
set(provider: string, credential: StoredCredential): void;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function readStoredOAuthCredential(providerId: string): StoredOAuthCredential | null {
|
|
43
|
+
const credential = piAgentRuntime.readStoredCredential
|
|
44
|
+
? piAgentRuntime.readStoredCredential(providerId)
|
|
45
|
+
: piAgentRuntime.AuthStorage?.create().get(providerId);
|
|
46
|
+
return credential?.type === "oauth" ? (credential as StoredOAuthCredential) : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Mutual exclusion with pi's own credential writes: use the same library
|
|
50
|
+
// (proper-lockfile) on the same file pi's FileAuthStorageBackend locks, with
|
|
51
|
+
// the same options. The shared implementation keeps the lock mtime fresh
|
|
52
|
+
// while held, recovers stale locks from crashed processes, and retries for
|
|
53
|
+
// ~40s so a live holder running a slow network refresh inside the lock is
|
|
54
|
+
// waited out.
|
|
55
|
+
async function acquireAuthFileLock(authPath: string): Promise<() => Promise<void>> {
|
|
56
|
+
return acquireFileLock(authPath, {
|
|
57
|
+
realpath: false,
|
|
58
|
+
stale: 30_000,
|
|
59
|
+
retries: { retries: 10, factor: 2, minTimeout: 100, maxTimeout: 10_000, randomize: true },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function writeStoredCredential(
|
|
64
|
+
providerId: string,
|
|
65
|
+
credential: StoredOAuthCredential,
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
const AuthStorage = piAgentRuntime.AuthStorage;
|
|
68
|
+
if (AuthStorage) {
|
|
69
|
+
AuthStorage.create().set(providerId, credential);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// pi >=0.80.8: no exported write path, so do a locked read-modify-write of
|
|
73
|
+
// auth.json in pi's on-disk format (indent 2). The read happens inside the
|
|
74
|
+
// lock so concurrent updates from other processes are never clobbered, and
|
|
75
|
+
// the explicit chmod covers pre-existing files (writeFileSync's mode only
|
|
76
|
+
// applies on creation).
|
|
77
|
+
const authPath = join(piAgentRuntime.getAgentDir(), "auth.json");
|
|
78
|
+
const release = await acquireAuthFileLock(authPath);
|
|
79
|
+
try {
|
|
80
|
+
let data: Record<string, unknown> = {};
|
|
81
|
+
try {
|
|
82
|
+
data = JSON.parse(readFileSync(authPath, "utf-8"));
|
|
83
|
+
} catch {
|
|
84
|
+
// Missing or unreadable auth.json — start from an empty store.
|
|
85
|
+
}
|
|
86
|
+
data[providerId] = credential;
|
|
87
|
+
writeFileSync(authPath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 0o600 });
|
|
88
|
+
chmodSync(authPath, 0o600);
|
|
89
|
+
} finally {
|
|
90
|
+
await release();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
12
93
|
|
|
13
94
|
import { CLIENT_ID, PROVIDER_ID, RETRYABLE_REFRESH_STATUSES, getOAuthHost } from "./constants.ts";
|
|
14
95
|
import { getCommonHeaders } from "./device.ts";
|
|
@@ -424,8 +505,8 @@ export async function refreshKimiCodeToken(
|
|
|
424
505
|
// `expires` is in the past. If the server rotates/revokes the access token
|
|
425
506
|
// before that (common with short-lived session tokens), every request keeps
|
|
426
507
|
// returning 401. The streaming handler invokes refreshKimiAuthToken on the
|
|
427
|
-
// first auth error to force a refresh
|
|
428
|
-
//
|
|
508
|
+
// first auth error to force a refresh and persist the new credentials, and
|
|
509
|
+
// retry once.
|
|
429
510
|
// =============================================================================
|
|
430
511
|
|
|
431
512
|
export function getKimiApiKey(credentials: OAuthCredentials): string {
|
|
@@ -453,9 +534,7 @@ export async function refreshKimiAuthToken(
|
|
|
453
534
|
): Promise<string | null> {
|
|
454
535
|
try {
|
|
455
536
|
const kimiCred = readKimiCliCredentials();
|
|
456
|
-
const
|
|
457
|
-
const piCred = storage.get(PROVIDER_ID);
|
|
458
|
-
const piOAuth = piCred?.type === "oauth" ? piCred : null;
|
|
537
|
+
const piOAuth = readStoredOAuthCredential(PROVIDER_ID);
|
|
459
538
|
|
|
460
539
|
if (piOAuth) {
|
|
461
540
|
if (piOAuth.access !== currentKey && Date.now() < piOAuth.expires) {
|
|
@@ -471,14 +550,14 @@ export async function refreshKimiAuthToken(
|
|
|
471
550
|
if (!kimiCred?.refresh_token || kimiCred.refresh_token === piOAuth.refresh) throw error;
|
|
472
551
|
const kimiExpiresMs = (kimiCred.expires_at ?? 0) * 1000;
|
|
473
552
|
if (kimiCred.access_token !== currentKey && Date.now() < kimiExpiresMs) {
|
|
474
|
-
const recovered:
|
|
553
|
+
const recovered: StoredOAuthCredential = {
|
|
475
554
|
...piOAuth,
|
|
476
555
|
type: "oauth",
|
|
477
556
|
access: kimiCred.access_token!,
|
|
478
557
|
refresh: kimiCred.refresh_token,
|
|
479
558
|
expires: kimiExpiresMs,
|
|
480
559
|
};
|
|
481
|
-
|
|
560
|
+
await writeStoredCredential(PROVIDER_ID, recovered);
|
|
482
561
|
console.error("[kimi-coding] auth refresh: recovered newer kimi-code token");
|
|
483
562
|
return recovered.access;
|
|
484
563
|
}
|
|
@@ -487,14 +566,14 @@ export async function refreshKimiAuthToken(
|
|
|
487
566
|
}
|
|
488
567
|
|
|
489
568
|
const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
|
|
490
|
-
const newCred:
|
|
569
|
+
const newCred: StoredOAuthCredential = {
|
|
491
570
|
...piOAuth,
|
|
492
571
|
type: "oauth",
|
|
493
572
|
access: refreshed.access_token,
|
|
494
573
|
refresh: refreshed.refresh_token,
|
|
495
574
|
expires: newExpiresMs,
|
|
496
575
|
};
|
|
497
|
-
|
|
576
|
+
await writeStoredCredential(PROVIDER_ID, newCred);
|
|
498
577
|
writeKimiCodeCredentials(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
|
|
499
578
|
console.error("[kimi-coding] auth refresh: new token persisted");
|
|
500
579
|
return newCred.access;
|
package/src/stream.ts
CHANGED
|
@@ -12,11 +12,46 @@ import type {
|
|
|
12
12
|
Model,
|
|
13
13
|
SimpleStreamOptions,
|
|
14
14
|
} from "@earendil-works/pi-ai";
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
import * as piAi from "@earendil-works/pi-ai";
|
|
16
|
+
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
17
|
+
|
|
18
|
+
type KimiStreamSimple = (
|
|
19
|
+
model: Model<Api>,
|
|
20
|
+
context: Context,
|
|
21
|
+
options?: SimpleStreamOptions,
|
|
22
|
+
) => AssistantMessageEventStream;
|
|
23
|
+
|
|
24
|
+
// pi 0.80.8 unexported the global streamSimple* functions. Depending on how
|
|
25
|
+
// this module is loaded, the pi-ai root now offers one of three shapes:
|
|
26
|
+
// - pi extension loader (jiti/Bun): root aliases to the compat entrypoint,
|
|
27
|
+
// which exposes lazy per-API factories (anthropicMessagesApi et al.)
|
|
28
|
+
// - pi <=0.79: root only has the legacy streamSimple* globals
|
|
29
|
+
// - plain Node with pi-ai >=0.80.8 (tests, SDK use): root has neither, but
|
|
30
|
+
// lazyApi + a dynamic subpath import reaches the same implementation
|
|
31
|
+
// Detect whichever shape is present so the extension loads in all three.
|
|
32
|
+
const piAiRuntime = piAi as unknown as {
|
|
33
|
+
anthropicMessagesApi?: () => { streamSimple: KimiStreamSimple };
|
|
34
|
+
openAICompletionsApi?: () => { streamSimple: KimiStreamSimple };
|
|
35
|
+
streamSimpleAnthropic?: KimiStreamSimple;
|
|
36
|
+
streamSimpleOpenAICompletions?: KimiStreamSimple;
|
|
37
|
+
lazyApi?: (load: () => Promise<object>) => { streamSimple: KimiStreamSimple };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// The ./api/* subpath exports only exist on pi-ai >=0.80.8, so a literal
|
|
41
|
+
// specifier would fail type resolution against older pi-ai. Computed
|
|
42
|
+
// specifiers keep the imports invisible to TypeScript; they are only ever
|
|
43
|
+
// evaluated in plain-Node environments where the subpaths do resolve.
|
|
44
|
+
const anthropicMessagesModule = "@earendil-works/pi-ai/api/anthropic-messages";
|
|
45
|
+
const openAICompletionsModule = "@earendil-works/pi-ai/api/openai-completions";
|
|
46
|
+
|
|
47
|
+
const streamSimpleAnthropic: KimiStreamSimple =
|
|
48
|
+
piAiRuntime.anthropicMessagesApi?.().streamSimple ??
|
|
49
|
+
piAiRuntime.streamSimpleAnthropic ??
|
|
50
|
+
piAiRuntime.lazyApi?.(() => import(anthropicMessagesModule)).streamSimple!;
|
|
51
|
+
const streamSimpleOpenAICompletions: KimiStreamSimple =
|
|
52
|
+
piAiRuntime.openAICompletionsApi?.().streamSimple ??
|
|
53
|
+
piAiRuntime.streamSimpleOpenAICompletions ??
|
|
54
|
+
piAiRuntime.lazyApi?.(() => import(openAICompletionsModule)).streamSimple!;
|
|
20
55
|
import {
|
|
21
56
|
DEFAULT_KIMI_CODE_CONFIG,
|
|
22
57
|
type KimiCodeConfig,
|
|
@@ -162,7 +197,16 @@ export function resolveKimiApiKey(apiKey: string | undefined): string {
|
|
|
162
197
|
return apiKey || process.env.KIMI_API_KEY || "";
|
|
163
198
|
}
|
|
164
199
|
|
|
165
|
-
|
|
200
|
+
// SimpleStreamOptions.headers is Record<string, string> on pi <=0.79 and
|
|
201
|
+
// ProviderHeaders (Record<string, string | null>, null = suppress the header
|
|
202
|
+
// when pi-ai assembles the request) on >=0.80.8. Accept and preserve both
|
|
203
|
+
// shapes; defined locally because ProviderHeaders is not exported on old pi.
|
|
204
|
+
type HeaderMap = Record<string, string | null>;
|
|
205
|
+
|
|
206
|
+
export function mergeKimiRequestHeaders(headers?: HeaderMap): HeaderMap {
|
|
207
|
+
// Caller values override our Kimi defaults. Nulls are retained rather than
|
|
208
|
+
// deleted so the suppression semantics reach pi-ai, which strips null
|
|
209
|
+
// entries (including its own defaults) when building the request.
|
|
166
210
|
return { ...getKimiProviderHeaders(), ...headers };
|
|
167
211
|
}
|
|
168
212
|
|
|
@@ -231,7 +275,9 @@ export function streamSimpleKimi(
|
|
|
231
275
|
return {
|
|
232
276
|
...options,
|
|
233
277
|
...apiKeyOverride,
|
|
234
|
-
headers:
|
|
278
|
+
// Cast to whatever headers shape the installed pi-ai declares:
|
|
279
|
+
// Record<string, string> on pi <=0.79, ProviderHeaders on >=0.80.8.
|
|
280
|
+
headers: mergeKimiRequestHeaders(options?.headers) as SimpleStreamOptions["headers"],
|
|
235
281
|
onPayload: async (payload, modelData) => {
|
|
236
282
|
let nextPayload: unknown = payload;
|
|
237
283
|
|
package/src/tools/common.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
|
|
4
4
|
import { PROVIDER_ID, getBaseUrl } from "../constants.ts";
|
|
5
5
|
import { getKimiProviderHeaders } from "../device.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
KIMI_LOGIN_REQUIRED_MESSAGE,
|
|
8
|
+
readStoredOAuthCredential,
|
|
9
|
+
refreshKimiAuthToken,
|
|
10
|
+
} from "../oauth.ts";
|
|
7
11
|
|
|
8
12
|
export const KIMI_TOOL_TIMEOUT_MS = 180_000;
|
|
9
13
|
|
|
@@ -19,9 +23,7 @@ export interface BuildKimiToolOptions {
|
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
function defaultGetAccessToken(): string | null {
|
|
22
|
-
|
|
23
|
-
if (!credential || credential.type !== "oauth") return null;
|
|
24
|
-
return credential.access;
|
|
26
|
+
return readStoredOAuthCredential(PROVIDER_ID)?.access ?? null;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export function buildKimiToolDeps(options: BuildKimiToolOptions = {}): KimiToolDeps {
|
package/src/usage.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { AuthStorage } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
|
|
3
1
|
import { PROVIDER_ID, getBaseUrl } from "./constants.ts";
|
|
4
2
|
import { getKimiProviderHeaders } from "./device.ts";
|
|
5
|
-
import { refreshKimiAuthToken } from "./oauth.ts";
|
|
3
|
+
import { readStoredOAuthCredential, refreshKimiAuthToken } from "./oauth.ts";
|
|
6
4
|
|
|
7
5
|
const MEMBERSHIP_LEVEL_NAMES: Record<string, string> = {
|
|
8
6
|
LEVEL_FREE: "Free",
|
|
@@ -52,8 +50,8 @@ interface BoosterWalletInfo {
|
|
|
52
50
|
const FIXED_POINT_CENTS = 1_000_000n;
|
|
53
51
|
|
|
54
52
|
export function getKimiUsageToken(): string | null {
|
|
55
|
-
const credential =
|
|
56
|
-
if (credential?.
|
|
53
|
+
const credential = readStoredOAuthCredential(PROVIDER_ID);
|
|
54
|
+
if (credential?.access) return credential.access;
|
|
57
55
|
const apiKey = process.env.KIMI_API_KEY?.trim();
|
|
58
56
|
return apiKey || null;
|
|
59
57
|
}
|