pi-provider-kimi-code 0.6.2 → 0.6.5
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 +7 -5
- package/index.ts +308 -469
- package/package.json +4 -4
- package/src/config.ts +18 -5
- package/src/constants.ts +2 -2
- package/src/device.ts +41 -0
- package/src/models.ts +100 -28
- package/src/oauth.ts +137 -35
- package/src/payload.ts +38 -25
- package/src/project-trust.ts +24 -0
- package/src/settings-ui.ts +204 -0
- package/src/stream.ts +25 -6
- package/src/tools/common.ts +6 -4
- package/src/tools/moonshot.ts +13 -35
- package/src/usage.ts +366 -0
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.5",
|
|
4
4
|
"description": "Use Kimi Code in Pi without losing the Kimi-specific bits",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-provider",
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"pre-commit": "prek run"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@earendil-works/pi-ai": "^0.
|
|
54
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
55
|
-
"@earendil-works/pi-tui": "^0.
|
|
53
|
+
"@earendil-works/pi-ai": "^0.79.10",
|
|
54
|
+
"@earendil-works/pi-coding-agent": "^0.79.10",
|
|
55
|
+
"@earendil-works/pi-tui": "^0.79.10",
|
|
56
56
|
"@j178/prek": "^0.4.4",
|
|
57
57
|
"@types/node": "^24.0.0",
|
|
58
58
|
"oxfmt": "^0.44.0",
|
package/src/config.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type KimiToolName = (typeof KIMI_TOOL_NAMES)[number];
|
|
|
9
9
|
|
|
10
10
|
export type KimiConfigSource = "runtime" | "env" | "project" | "home" | "default";
|
|
11
11
|
|
|
12
|
+
export const PI_CONFIG_DIR_NAME = ".pi";
|
|
13
|
+
|
|
12
14
|
export type KimiInputModality = "text" | "image" | "video";
|
|
13
15
|
|
|
14
16
|
export interface ModelReasoningEntry {
|
|
@@ -36,6 +38,8 @@ export interface ModelConfig {
|
|
|
36
38
|
|
|
37
39
|
export interface KimiResolvedModelConfig extends ModelConfig {
|
|
38
40
|
supportsThinkingType?: "only" | "no" | "both";
|
|
41
|
+
supportEfforts?: string[];
|
|
42
|
+
defaultEffort?: string;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
export interface KimiCodeConfig {
|
|
@@ -83,6 +87,7 @@ export interface LoadKimiCodeConfigOptions {
|
|
|
83
87
|
cwd: string;
|
|
84
88
|
home: string;
|
|
85
89
|
env?: NodeJS.ProcessEnv;
|
|
90
|
+
includeProject?: boolean;
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
export class ConfigError extends Error {
|
|
@@ -147,11 +152,11 @@ export function getRuntimeKimiCodeConfigOverride(): KimiCodeConfigPatch {
|
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
export function getGlobalKimiCodeConfigPath(home: string): string {
|
|
150
|
-
return join(home,
|
|
155
|
+
return join(home, PI_CONFIG_DIR_NAME, "providers", PROVIDER_ID, "config.json");
|
|
151
156
|
}
|
|
152
157
|
|
|
153
158
|
export function getProjectKimiCodeConfigPath(cwd: string): string {
|
|
154
|
-
return join(cwd,
|
|
159
|
+
return join(cwd, PI_CONFIG_DIR_NAME, "providers", PROVIDER_ID, "config.json");
|
|
155
160
|
}
|
|
156
161
|
|
|
157
162
|
export function kimiCodeConfigPath(home: string): string {
|
|
@@ -483,15 +488,23 @@ function buildSources(
|
|
|
483
488
|
function loadLayers(
|
|
484
489
|
options: LoadKimiCodeConfigOptions,
|
|
485
490
|
): Array<{ source: KimiConfigSource; config: Record<string, unknown> }> {
|
|
486
|
-
|
|
491
|
+
const layers: Array<{ source: KimiConfigSource; config: Record<string, unknown> }> = [
|
|
487
492
|
{ source: "home", config: readConfigFile(getGlobalKimiCodeConfigPath(options.home)) },
|
|
488
|
-
|
|
493
|
+
];
|
|
494
|
+
if (options.includeProject !== false) {
|
|
495
|
+
layers.push({
|
|
496
|
+
source: "project",
|
|
497
|
+
config: readConfigFile(getProjectKimiCodeConfigPath(options.cwd)),
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
layers.push(
|
|
489
501
|
{
|
|
490
502
|
source: "env",
|
|
491
503
|
config: envConfigPatch(options.env ?? process.env) as Record<string, unknown>,
|
|
492
504
|
},
|
|
493
505
|
{ source: "runtime", config: runtimeOverride as Record<string, unknown> },
|
|
494
|
-
|
|
506
|
+
);
|
|
507
|
+
return layers;
|
|
495
508
|
}
|
|
496
509
|
|
|
497
510
|
export function loadKimiCodeConfig(
|
package/src/constants.ts
CHANGED
|
@@ -41,12 +41,12 @@ 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.5";
|
|
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
|
|
48
48
|
// syncing with https://github.com/anthropics/kimi-code.
|
|
49
|
-
export const KIMI_UPSTREAM_VERSION = "0.
|
|
49
|
+
export const KIMI_UPSTREAM_VERSION = "0.23.4";
|
|
50
50
|
export const KIMI_CODE_USER_AGENT = `kimi-code-cli/${KIMI_UPSTREAM_VERSION}`;
|
|
51
51
|
export const KIMI_PLATFORM = "kimi_code_cli";
|
|
52
52
|
|
package/src/device.ts
CHANGED
|
@@ -152,6 +152,22 @@ function getStableDeviceId(): string {
|
|
|
152
152
|
return DEVICE_ID;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
export function parseKimiCodeCustomHeaders(
|
|
156
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
157
|
+
): Record<string, string> {
|
|
158
|
+
const raw = env.KIMI_CODE_CUSTOM_HEADERS?.trim();
|
|
159
|
+
if (!raw) return {};
|
|
160
|
+
const headers: Record<string, string> = {};
|
|
161
|
+
for (const line of raw.split("\n")) {
|
|
162
|
+
const colon = line.indexOf(":");
|
|
163
|
+
if (colon < 0) continue;
|
|
164
|
+
const name = line.slice(0, colon).trim();
|
|
165
|
+
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) continue;
|
|
166
|
+
headers[name] = asciiHeaderValue(line.slice(colon + 1));
|
|
167
|
+
}
|
|
168
|
+
return headers;
|
|
169
|
+
}
|
|
170
|
+
|
|
155
171
|
export function getCommonHeaders(): Record<string, string> {
|
|
156
172
|
const headers = {
|
|
157
173
|
"User-Agent": KIMI_CODE_USER_AGENT,
|
|
@@ -166,3 +182,28 @@ export function getCommonHeaders(): Record<string, string> {
|
|
|
166
182
|
Object.entries(headers).map(([key, value]) => [key, asciiHeaderValue(value)]),
|
|
167
183
|
) as Record<string, string>;
|
|
168
184
|
}
|
|
185
|
+
|
|
186
|
+
const RESERVED_PROVIDER_HEADERS = new Set([
|
|
187
|
+
"accept",
|
|
188
|
+
"anthropic-version",
|
|
189
|
+
"authorization",
|
|
190
|
+
"content-type",
|
|
191
|
+
"user-agent",
|
|
192
|
+
"x-api-key",
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
function isReservedProviderHeader(name: string): boolean {
|
|
196
|
+
const normalized = name.toLowerCase();
|
|
197
|
+
return RESERVED_PROVIDER_HEADERS.has(normalized) || normalized.startsWith("x-msh-");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function getKimiProviderHeaders(
|
|
201
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
202
|
+
): Record<string, string> {
|
|
203
|
+
const customHeaders = Object.fromEntries(
|
|
204
|
+
Object.entries(parseKimiCodeCustomHeaders(env)).filter(
|
|
205
|
+
([name]) => !isReservedProviderHeader(name),
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
return { ...customHeaders, ...getCommonHeaders() };
|
|
209
|
+
}
|
package/src/models.ts
CHANGED
|
@@ -5,9 +5,9 @@ import type { Api, Model, OAuthCredentials } from "@earendil-works/pi-ai";
|
|
|
5
5
|
import type { KimiInputModality, KimiResolvedModelConfig, ModelConfig } from "./config.ts";
|
|
6
6
|
|
|
7
7
|
import { type KimiWireProtocol, getBaseUrl } from "./constants.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { getKimiProviderHeaders } from "./device.ts";
|
|
9
9
|
|
|
10
|
-
export interface
|
|
10
|
+
export interface KimiModelMetadata {
|
|
11
11
|
wireModelId?: string;
|
|
12
12
|
modelDisplay?: string;
|
|
13
13
|
contextLength?: number;
|
|
@@ -15,6 +15,13 @@ export interface KimiOAuthExtras {
|
|
|
15
15
|
supportsImageIn?: boolean;
|
|
16
16
|
supportsVideoIn?: boolean;
|
|
17
17
|
supportsThinkingType?: "only" | "no" | "both";
|
|
18
|
+
protocol?: KimiWireProtocol;
|
|
19
|
+
supportEfforts?: string[];
|
|
20
|
+
defaultEffort?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface KimiOAuthExtras extends KimiModelMetadata {
|
|
24
|
+
modelCatalog?: Record<string, KimiModelMetadata>;
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
export type KimiOAuthCredentials = OAuthCredentials & KimiOAuthExtras;
|
|
@@ -47,6 +54,9 @@ function mergeInputModalities(
|
|
|
47
54
|
const COST_STANDARD = { input: 0.897, output: 3.724, cacheRead: 0.179, cacheWrite: 0.897 };
|
|
48
55
|
const COST_HIGH_SPEED = { input: 1.793, output: 7.448, cacheRead: 0.359, cacheWrite: 1.793 };
|
|
49
56
|
|
|
57
|
+
export const KIMI_CODING_MODEL_ID = "kimi-for-coding";
|
|
58
|
+
export const KIMI_CODING_HIGHSPEED_MODEL_ID = "kimi-for-coding-highspeed";
|
|
59
|
+
|
|
50
60
|
function resolveModelCost(modelDisplay: string | undefined): {
|
|
51
61
|
input: number;
|
|
52
62
|
output: number;
|
|
@@ -57,13 +67,17 @@ function resolveModelCost(modelDisplay: string | undefined): {
|
|
|
57
67
|
return COST_STANDARD;
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
export function buildKimiModelFromConfig(
|
|
70
|
+
export function buildKimiModelFromConfig(
|
|
71
|
+
config: ModelConfig,
|
|
72
|
+
modelId = KIMI_CODING_MODEL_ID,
|
|
73
|
+
): Model<Api> {
|
|
74
|
+
const isHighSpeed = modelId === KIMI_CODING_HIGHSPEED_MODEL_ID;
|
|
61
75
|
return {
|
|
62
|
-
id:
|
|
63
|
-
name: "Kimi for Coding",
|
|
76
|
+
id: modelId,
|
|
77
|
+
name: isHighSpeed ? "Kimi for Coding High Speed" : "Kimi for Coding",
|
|
64
78
|
reasoning: config.reasoning,
|
|
65
79
|
input: [...config.input] as unknown as ("text" | "image" | "video")[],
|
|
66
|
-
cost: { ...COST_STANDARD },
|
|
80
|
+
cost: { ...(isHighSpeed ? COST_HIGH_SPEED : COST_STANDARD) },
|
|
67
81
|
contextWindow: config.contextWindow,
|
|
68
82
|
maxTokens: config.maxTokens,
|
|
69
83
|
} as Model<Api>;
|
|
@@ -86,6 +100,8 @@ export function resolveKimiModelConfig(
|
|
|
86
100
|
if (typeof extras.supportsImageIn === "boolean" || typeof extras.supportsVideoIn === "boolean") {
|
|
87
101
|
resolved.input = mergeInputModalities(resolved.input, extras);
|
|
88
102
|
}
|
|
103
|
+
if (extras.supportEfforts) resolved.supportEfforts = [...extras.supportEfforts];
|
|
104
|
+
if (extras.defaultEffort) resolved.defaultEffort = extras.defaultEffort;
|
|
89
105
|
return resolved;
|
|
90
106
|
}
|
|
91
107
|
|
|
@@ -97,6 +113,8 @@ interface KimiServerModel {
|
|
|
97
113
|
supports_image_in?: unknown;
|
|
98
114
|
supports_video_in?: unknown;
|
|
99
115
|
supports_thinking_type?: unknown;
|
|
116
|
+
protocol?: unknown;
|
|
117
|
+
think_efforts?: unknown;
|
|
100
118
|
}
|
|
101
119
|
|
|
102
120
|
function parseSupportsThinkingType(value: unknown): "only" | "no" | "both" | undefined {
|
|
@@ -104,6 +122,54 @@ function parseSupportsThinkingType(value: unknown): "only" | "no" | "both" | und
|
|
|
104
122
|
return undefined;
|
|
105
123
|
}
|
|
106
124
|
|
|
125
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
126
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseThinkEfforts(value: unknown): {
|
|
130
|
+
supportEfforts?: string[];
|
|
131
|
+
defaultEffort?: string;
|
|
132
|
+
} {
|
|
133
|
+
if (!isRecord(value) || value.support !== true) return {};
|
|
134
|
+
const validEfforts = Array.isArray(value.valid_efforts)
|
|
135
|
+
? value.valid_efforts.filter(
|
|
136
|
+
(effort): effort is string => typeof effort === "string" && !!effort,
|
|
137
|
+
)
|
|
138
|
+
: [];
|
|
139
|
+
return {
|
|
140
|
+
...(validEfforts.length > 0 ? { supportEfforts: validEfforts } : {}),
|
|
141
|
+
...(typeof value.default_effort === "string" && value.default_effort
|
|
142
|
+
? { defaultEffort: value.default_effort }
|
|
143
|
+
: {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parseKimiModelMetadata(model: KimiServerModel): KimiModelMetadata | undefined {
|
|
148
|
+
if (typeof model.id !== "string" || !model.id) return undefined;
|
|
149
|
+
const metadata: KimiModelMetadata = { wireModelId: model.id };
|
|
150
|
+
if (typeof model.display_name === "string") metadata.modelDisplay = model.display_name;
|
|
151
|
+
if (typeof model.context_length === "number" && model.context_length > 0) {
|
|
152
|
+
metadata.contextLength = model.context_length;
|
|
153
|
+
}
|
|
154
|
+
const thinkingType = parseSupportsThinkingType(model.supports_thinking_type);
|
|
155
|
+
if (thinkingType) {
|
|
156
|
+
metadata.supportsThinkingType = thinkingType;
|
|
157
|
+
} else if (typeof model.supports_reasoning === "boolean") {
|
|
158
|
+
metadata.supportsReasoning = model.supports_reasoning;
|
|
159
|
+
}
|
|
160
|
+
if (typeof model.supports_image_in === "boolean") {
|
|
161
|
+
metadata.supportsImageIn = model.supports_image_in;
|
|
162
|
+
}
|
|
163
|
+
if (typeof model.supports_video_in === "boolean") {
|
|
164
|
+
metadata.supportsVideoIn = model.supports_video_in;
|
|
165
|
+
}
|
|
166
|
+
if (model.protocol === "openai" || model.protocol === "anthropic") {
|
|
167
|
+
metadata.protocol = model.protocol;
|
|
168
|
+
}
|
|
169
|
+
Object.assign(metadata, parseThinkEfforts(model.think_efforts));
|
|
170
|
+
return metadata;
|
|
171
|
+
}
|
|
172
|
+
|
|
107
173
|
export function buildModelsUrl(baseUrl: string): string {
|
|
108
174
|
const base = baseUrl.replace(/\/+$/, "");
|
|
109
175
|
return base.endsWith("/v1") ? `${base}/models` : `${base}/v1/models`;
|
|
@@ -127,7 +193,7 @@ export async function discoverKimiModelMetadata(
|
|
|
127
193
|
const response = await fetch(getModelsUrl(protocol), {
|
|
128
194
|
signal: controller.signal,
|
|
129
195
|
headers: {
|
|
130
|
-
...
|
|
196
|
+
...getKimiProviderHeaders(),
|
|
131
197
|
Authorization: `Bearer ${accessToken}`,
|
|
132
198
|
Accept: "application/json",
|
|
133
199
|
},
|
|
@@ -135,26 +201,18 @@ export async function discoverKimiModelMetadata(
|
|
|
135
201
|
if (!response.ok) return {};
|
|
136
202
|
const json = (await response.json()) as { data?: unknown };
|
|
137
203
|
const list = Array.isArray(json.data) ? (json.data as KimiServerModel[]) : [];
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
const thinkingType = parseSupportsThinkingType(preferred.supports_thinking_type);
|
|
146
|
-
if (thinkingType) {
|
|
147
|
-
extras.supportsThinkingType = thinkingType;
|
|
148
|
-
} else if (typeof preferred.supports_reasoning === "boolean") {
|
|
149
|
-
extras.supportsReasoning = preferred.supports_reasoning;
|
|
150
|
-
}
|
|
151
|
-
if (typeof preferred.supports_image_in === "boolean") {
|
|
152
|
-
extras.supportsImageIn = preferred.supports_image_in;
|
|
204
|
+
const supportedIds = new Set([KIMI_CODING_MODEL_ID, KIMI_CODING_HIGHSPEED_MODEL_ID]);
|
|
205
|
+
const modelCatalog: Record<string, KimiModelMetadata> = {};
|
|
206
|
+
for (const model of list) {
|
|
207
|
+
if (typeof model.id !== "string" || !supportedIds.has(model.id)) continue;
|
|
208
|
+
const metadata = parseKimiModelMetadata(model);
|
|
209
|
+
if (metadata) modelCatalog[model.id] = metadata;
|
|
153
210
|
}
|
|
154
|
-
|
|
155
|
-
|
|
211
|
+
const preferred = modelCatalog[KIMI_CODING_MODEL_ID];
|
|
212
|
+
if (!preferred) {
|
|
213
|
+
return Object.keys(modelCatalog).length > 0 ? { modelCatalog } : {};
|
|
156
214
|
}
|
|
157
|
-
return
|
|
215
|
+
return { ...preferred, modelCatalog };
|
|
158
216
|
} catch {
|
|
159
217
|
return {};
|
|
160
218
|
} finally {
|
|
@@ -162,12 +220,23 @@ export async function discoverKimiModelMetadata(
|
|
|
162
220
|
}
|
|
163
221
|
}
|
|
164
222
|
|
|
223
|
+
export function getKimiModelMetadata(extras: KimiOAuthExtras, modelId: string): KimiModelMetadata {
|
|
224
|
+
const discovered = extras.modelCatalog?.[modelId];
|
|
225
|
+
if (discovered) return discovered;
|
|
226
|
+
return modelId === KIMI_CODING_MODEL_ID ? extras : {};
|
|
227
|
+
}
|
|
228
|
+
|
|
165
229
|
export function applyKimiOAuthExtrasToModel(
|
|
166
230
|
model: Model<Api>,
|
|
167
|
-
extras:
|
|
231
|
+
extras: KimiModelMetadata,
|
|
168
232
|
): Model<Api> {
|
|
169
|
-
const next: Model<Api> & {
|
|
170
|
-
|
|
233
|
+
const next: Model<Api> & {
|
|
234
|
+
wireModelId?: string;
|
|
235
|
+
supportsThinkingType?: "only" | "no" | "both";
|
|
236
|
+
wireProtocol?: KimiWireProtocol;
|
|
237
|
+
supportEfforts?: string[];
|
|
238
|
+
defaultEffort?: string;
|
|
239
|
+
} = { ...model };
|
|
171
240
|
if (typeof extras.modelDisplay === "string" && extras.modelDisplay) {
|
|
172
241
|
next.name = extras.modelDisplay;
|
|
173
242
|
next.cost = resolveModelCost(extras.modelDisplay);
|
|
@@ -189,5 +258,8 @@ export function applyKimiOAuthExtrasToModel(
|
|
|
189
258
|
const input = mergeInputModalities(next.input as KimiInputModality[], extras);
|
|
190
259
|
(next as unknown as { input: string[] }).input = input;
|
|
191
260
|
}
|
|
261
|
+
if (extras.protocol) next.wireProtocol = extras.protocol;
|
|
262
|
+
if (extras.supportEfforts) next.supportEfforts = [...extras.supportEfforts];
|
|
263
|
+
if (extras.defaultEffort) next.defaultEffort = extras.defaultEffort;
|
|
192
264
|
return next;
|
|
193
265
|
}
|
package/src/oauth.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
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, readFileSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import os from "node:os";
|
|
7
|
-
import { join } from "node:path";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
8
|
|
|
9
9
|
import type { OAuthCredentials, OAuthLoginCallbacks } from "@earendil-works/pi-ai";
|
|
10
10
|
import type { OAuthCredential } from "@earendil-works/pi-coding-agent";
|
|
@@ -123,6 +123,16 @@ interface RefreshAccessTokenOptions {
|
|
|
123
123
|
sleep?: (ms: number) => Promise<void>;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
export const KIMI_LOGIN_REQUIRED_MESSAGE =
|
|
127
|
+
"Kimi Code authorization is no longer valid. Sign in again with /login kimi-coding.";
|
|
128
|
+
|
|
129
|
+
export class KimiLoginRequiredError extends Error {
|
|
130
|
+
constructor() {
|
|
131
|
+
super(KIMI_LOGIN_REQUIRED_MESSAGE);
|
|
132
|
+
this.name = "KimiLoginRequiredError";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
126
136
|
function sleep(ms: number): Promise<void> {
|
|
127
137
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
128
138
|
}
|
|
@@ -152,8 +162,15 @@ export async function refreshAccessToken(
|
|
|
152
162
|
|
|
153
163
|
if (!response.ok) {
|
|
154
164
|
const text = await response.text().catch(() => "");
|
|
155
|
-
|
|
156
|
-
|
|
165
|
+
let errorCode = "";
|
|
166
|
+
try {
|
|
167
|
+
const body = JSON.parse(text) as { error?: unknown };
|
|
168
|
+
if (typeof body.error === "string") errorCode = body.error;
|
|
169
|
+
} catch {
|
|
170
|
+
// Non-JSON OAuth errors are classified by status below.
|
|
171
|
+
}
|
|
172
|
+
if (response.status === 401 || response.status === 403 || errorCode === "invalid_grant") {
|
|
173
|
+
throw new KimiLoginRequiredError();
|
|
157
174
|
}
|
|
158
175
|
if (RETRYABLE_REFRESH_STATUSES.has(response.status)) {
|
|
159
176
|
throw new Error(`Token refresh retryable: ${response.status} ${text}`);
|
|
@@ -170,7 +187,7 @@ export async function refreshAccessToken(
|
|
|
170
187
|
} catch (error) {
|
|
171
188
|
lastError = error;
|
|
172
189
|
const message = error instanceof Error ? error.message : String(error);
|
|
173
|
-
if (
|
|
190
|
+
if (error instanceof KimiLoginRequiredError) throw error;
|
|
174
191
|
if (message.startsWith("Token refresh failed:")) throw error;
|
|
175
192
|
if (attempt < maxRetries - 1) {
|
|
176
193
|
await wait(2 ** attempt * 1000);
|
|
@@ -196,15 +213,19 @@ interface KimiCliCredentialsFile {
|
|
|
196
213
|
access_token?: string;
|
|
197
214
|
refresh_token?: string;
|
|
198
215
|
expires_at?: number; // Unix seconds (upstream convention)
|
|
216
|
+
scope?: string;
|
|
217
|
+
token_type?: string;
|
|
218
|
+
expires_in?: number;
|
|
199
219
|
}
|
|
200
220
|
|
|
201
|
-
function
|
|
221
|
+
function getKimiCodeCredentialPath(): string {
|
|
202
222
|
const kimiCodeHome = process.env.KIMI_CODE_HOME || join(os.homedir(), ".kimi-code");
|
|
223
|
+
return join(kimiCodeHome, "credentials", "kimi-code.json");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getKimiCredentialPaths(): string[] {
|
|
203
227
|
const shareDir = process.env.KIMI_SHARE_DIR || join(os.homedir(), ".kimi");
|
|
204
|
-
return [
|
|
205
|
-
join(kimiCodeHome, "credentials", "kimi-code.json"),
|
|
206
|
-
join(shareDir, "credentials", "kimi-code.json"),
|
|
207
|
-
];
|
|
228
|
+
return [getKimiCodeCredentialPath(), join(shareDir, "credentials", "kimi-code.json")];
|
|
208
229
|
}
|
|
209
230
|
|
|
210
231
|
function readKimiCliCredentials(): KimiCliCredentialsFile | null {
|
|
@@ -221,6 +242,25 @@ function readKimiCliCredentials(): KimiCliCredentialsFile | null {
|
|
|
221
242
|
return null;
|
|
222
243
|
}
|
|
223
244
|
|
|
245
|
+
function kimiCodeCredentialExists(): boolean {
|
|
246
|
+
return existsSync(getKimiCodeCredentialPath());
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function writeKimiCodeCredentials(access: string, refresh: string, expiresMs: number): void {
|
|
250
|
+
const path = getKimiCodeCredentialPath();
|
|
251
|
+
const dir = dirname(path);
|
|
252
|
+
mkdirSync(dir, { recursive: true });
|
|
253
|
+
const data: KimiCliCredentialsFile = {
|
|
254
|
+
access_token: access,
|
|
255
|
+
refresh_token: refresh,
|
|
256
|
+
expires_at: Math.floor(expiresMs / 1000),
|
|
257
|
+
scope: "",
|
|
258
|
+
token_type: "Bearer",
|
|
259
|
+
expires_in: Math.max(0, Math.floor((expiresMs - Date.now()) / 1000)),
|
|
260
|
+
};
|
|
261
|
+
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
|
|
262
|
+
}
|
|
263
|
+
|
|
224
264
|
async function tryReuseKimiCliCredentials(
|
|
225
265
|
callbacks: OAuthLoginCallbacks,
|
|
226
266
|
): Promise<KimiOAuthCredentials | null> {
|
|
@@ -247,11 +287,13 @@ async function tryReuseKimiCliCredentials(
|
|
|
247
287
|
callbacks.onProgress?.("Kimi access token expired, refreshing.");
|
|
248
288
|
try {
|
|
249
289
|
const token = await refreshAccessToken(data.refresh_token!);
|
|
290
|
+
const expiresMs = Date.now() + token.expires_in * 1000;
|
|
291
|
+
writeKimiCodeCredentials(token.access_token, token.refresh_token, expiresMs);
|
|
250
292
|
const extras = await discoverKimiModelMetadata(token.access_token);
|
|
251
293
|
return {
|
|
252
294
|
access: token.access_token,
|
|
253
295
|
refresh: token.refresh_token,
|
|
254
|
-
expires:
|
|
296
|
+
expires: expiresMs,
|
|
255
297
|
...extras,
|
|
256
298
|
};
|
|
257
299
|
} catch {
|
|
@@ -319,11 +361,13 @@ export async function loginKimiCode(callbacks: OAuthLoginCallbacks): Promise<Kim
|
|
|
319
361
|
}
|
|
320
362
|
|
|
321
363
|
if (token) {
|
|
364
|
+
const expiresMs = Date.now() + token.expires_in * 1000;
|
|
365
|
+
writeKimiCodeCredentials(token.access_token, token.refresh_token, expiresMs);
|
|
322
366
|
const extras = await discoverKimiModelMetadata(token.access_token);
|
|
323
367
|
return {
|
|
324
368
|
access: token.access_token,
|
|
325
369
|
refresh: token.refresh_token,
|
|
326
|
-
expires:
|
|
370
|
+
expires: expiresMs,
|
|
327
371
|
...extras,
|
|
328
372
|
};
|
|
329
373
|
}
|
|
@@ -335,12 +379,24 @@ export async function loginKimiCode(callbacks: OAuthLoginCallbacks): Promise<Kim
|
|
|
335
379
|
export async function refreshKimiCodeToken(
|
|
336
380
|
credentials: OAuthCredentials,
|
|
337
381
|
): Promise<KimiOAuthCredentials> {
|
|
338
|
-
const
|
|
382
|
+
const kimiCred = kimiCodeCredentialExists() ? readKimiCliCredentials() : null;
|
|
383
|
+
let token: Awaited<ReturnType<typeof refreshAccessToken>>;
|
|
384
|
+
try {
|
|
385
|
+
token = await refreshAccessToken(credentials.refresh);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (kimiCred?.refresh_token && kimiCred.refresh_token !== credentials.refresh) {
|
|
388
|
+
token = await refreshAccessToken(kimiCred.refresh_token);
|
|
389
|
+
} else {
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const expiresMs = Date.now() + token.expires_in * 1000;
|
|
394
|
+
writeKimiCodeCredentials(token.access_token, token.refresh_token, expiresMs);
|
|
339
395
|
const extras = await discoverKimiModelMetadata(token.access_token);
|
|
340
396
|
return {
|
|
341
397
|
access: token.access_token,
|
|
342
398
|
refresh: token.refresh_token,
|
|
343
|
-
expires:
|
|
399
|
+
expires: expiresMs,
|
|
344
400
|
...extras,
|
|
345
401
|
};
|
|
346
402
|
}
|
|
@@ -356,6 +412,10 @@ export async function refreshKimiCodeToken(
|
|
|
356
412
|
// new credentials under a file lock), and retry once.
|
|
357
413
|
// =============================================================================
|
|
358
414
|
|
|
415
|
+
export function getKimiApiKey(credentials: OAuthCredentials): string {
|
|
416
|
+
return credentials.access;
|
|
417
|
+
}
|
|
418
|
+
|
|
359
419
|
export function isKimiAuthErrorMessage(message: unknown): boolean {
|
|
360
420
|
const text = String(message ?? "").toLowerCase();
|
|
361
421
|
return (
|
|
@@ -368,37 +428,79 @@ export function isKimiAuthErrorMessage(message: unknown): boolean {
|
|
|
368
428
|
|
|
369
429
|
export async function refreshKimiAuthToken(currentKey: string): Promise<string | null> {
|
|
370
430
|
try {
|
|
431
|
+
const kimiCred = readKimiCliCredentials();
|
|
371
432
|
const storage = AuthStorage.create();
|
|
372
|
-
const
|
|
373
|
-
|
|
433
|
+
const piCred = storage.get(PROVIDER_ID);
|
|
434
|
+
const piOAuth = piCred?.type === "oauth" ? piCred : null;
|
|
435
|
+
|
|
436
|
+
if (piOAuth) {
|
|
437
|
+
if (piOAuth.access !== currentKey && Date.now() < piOAuth.expires) {
|
|
438
|
+
console.error("[kimi-coding] auth refresh: reusing newer on-disk token");
|
|
439
|
+
return piOAuth.access;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
console.error("[kimi-coding] auth refresh: requesting new access token");
|
|
443
|
+
let refreshed: Awaited<ReturnType<typeof refreshAccessToken>>;
|
|
444
|
+
try {
|
|
445
|
+
refreshed = await refreshAccessToken(piOAuth.refresh);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
if (!kimiCred?.refresh_token || kimiCred.refresh_token === piOAuth.refresh) throw error;
|
|
448
|
+
const kimiExpiresMs = (kimiCred.expires_at ?? 0) * 1000;
|
|
449
|
+
if (kimiCred.access_token !== currentKey && Date.now() < kimiExpiresMs) {
|
|
450
|
+
const recovered: OAuthCredential = {
|
|
451
|
+
...piOAuth,
|
|
452
|
+
type: "oauth",
|
|
453
|
+
access: kimiCred.access_token!,
|
|
454
|
+
refresh: kimiCred.refresh_token,
|
|
455
|
+
expires: kimiExpiresMs,
|
|
456
|
+
};
|
|
457
|
+
storage.set(PROVIDER_ID, recovered);
|
|
458
|
+
console.error("[kimi-coding] auth refresh: recovered newer kimi-code token");
|
|
459
|
+
return recovered.access;
|
|
460
|
+
}
|
|
461
|
+
console.error("[kimi-coding] auth refresh: pi token rejected, trying kimi-code token");
|
|
462
|
+
refreshed = await refreshAccessToken(kimiCred.refresh_token);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
|
|
466
|
+
const newCred: OAuthCredential = {
|
|
467
|
+
...piOAuth,
|
|
468
|
+
type: "oauth",
|
|
469
|
+
access: refreshed.access_token,
|
|
470
|
+
refresh: refreshed.refresh_token,
|
|
471
|
+
expires: newExpiresMs,
|
|
472
|
+
};
|
|
473
|
+
storage.set(PROVIDER_ID, newCred);
|
|
474
|
+
writeKimiCodeCredentials(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
|
|
475
|
+
console.error("[kimi-coding] auth refresh: new token persisted");
|
|
476
|
+
return newCred.access;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (!kimiCred) {
|
|
374
480
|
console.error(
|
|
375
481
|
`[kimi-coding] auth refresh skipped: no OAuth credentials for ${PROVIDER_ID} on disk`,
|
|
376
482
|
);
|
|
377
483
|
return null;
|
|
378
484
|
}
|
|
379
485
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
console.error("[kimi-coding] auth refresh: reusing newer on-disk token");
|
|
385
|
-
return cred.access;
|
|
486
|
+
const kimiExpiresMs = (kimiCred.expires_at ?? 0) * 1000;
|
|
487
|
+
if (kimiCred.access_token !== currentKey && Date.now() < kimiExpiresMs) {
|
|
488
|
+
console.error("[kimi-coding] auth refresh: reusing newer kimi-code token");
|
|
489
|
+
return kimiCred.access_token!;
|
|
386
490
|
}
|
|
387
491
|
|
|
388
|
-
console.error("[kimi-coding] auth refresh: requesting new access token");
|
|
389
|
-
const refreshed = await refreshAccessToken(
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
refresh: refreshed.refresh_token,
|
|
395
|
-
expires: Date.now() + refreshed.expires_in * 1000,
|
|
396
|
-
};
|
|
397
|
-
storage.set(PROVIDER_ID, newCred);
|
|
398
|
-
console.error("[kimi-coding] auth refresh: new token persisted");
|
|
399
|
-
return newCred.access;
|
|
492
|
+
console.error("[kimi-coding] auth refresh: requesting new access token via kimi-code");
|
|
493
|
+
const refreshed = await refreshAccessToken(kimiCred.refresh_token!);
|
|
494
|
+
const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
|
|
495
|
+
writeKimiCodeCredentials(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
|
|
496
|
+
console.error("[kimi-coding] auth refresh: new token persisted to kimi-code");
|
|
497
|
+
return refreshed.access_token;
|
|
400
498
|
} catch (err) {
|
|
401
|
-
|
|
499
|
+
if (err instanceof KimiLoginRequiredError) {
|
|
500
|
+
console.error(`[kimi-coding] ${KIMI_LOGIN_REQUIRED_MESSAGE}`);
|
|
501
|
+
} else {
|
|
502
|
+
console.error("[kimi-coding] auth refresh failed:", err);
|
|
503
|
+
}
|
|
402
504
|
return null;
|
|
403
505
|
}
|
|
404
506
|
}
|