pi-provider-kimi-code 0.6.3 → 0.6.6
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 +35 -21
- package/index.ts +129 -23
- package/package.json +1 -1
- package/src/config.ts +5 -2
- package/src/constants.ts +3 -3
- package/src/device.ts +41 -0
- package/src/models.ts +157 -29
- package/src/oauth.ts +168 -39
- package/src/payload.ts +60 -25
- package/src/stream.ts +25 -6
- package/src/tools/common.ts +6 -4
- package/src/tools/moonshot.ts +13 -35
- package/src/usage.ts +133 -13
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,14 @@ 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>;
|
|
25
|
+
modelCatalogVersion?: number;
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
export type KimiOAuthCredentials = OAuthCredentials & KimiOAuthExtras;
|
|
@@ -47,6 +55,49 @@ function mergeInputModalities(
|
|
|
47
55
|
const COST_STANDARD = { input: 0.897, output: 3.724, cacheRead: 0.179, cacheWrite: 0.897 };
|
|
48
56
|
const COST_HIGH_SPEED = { input: 1.793, output: 7.448, cacheRead: 0.359, cacheWrite: 1.793 };
|
|
49
57
|
|
|
58
|
+
export const KIMI_CODING_MODEL_ID = "kimi-for-coding";
|
|
59
|
+
export const KIMI_CODING_HIGHSPEED_MODEL_ID = "kimi-for-coding-highspeed";
|
|
60
|
+
export const KIMI_K3_MODEL_ID = "k3";
|
|
61
|
+
export const KIMI_MODEL_CATALOG_VERSION = 1;
|
|
62
|
+
|
|
63
|
+
const KIMI_K3_MODERATO_CONTEXT_WINDOW = 262144;
|
|
64
|
+
const KIMI_MEMBERSHIP_RANK: Readonly<Record<string, number>> = {
|
|
65
|
+
LEVEL_FREE: 0,
|
|
66
|
+
LEVEL_BASIC: 1,
|
|
67
|
+
LEVEL_STANDARD: 2,
|
|
68
|
+
LEVEL_INTERMEDIATE: 3,
|
|
69
|
+
LEVEL_ADVANCED: 4,
|
|
70
|
+
LEVEL_PREMIUM: 5,
|
|
71
|
+
};
|
|
72
|
+
const KIMI_MODERATO_RANK = KIMI_MEMBERSHIP_RANK.LEVEL_STANDARD;
|
|
73
|
+
const KIMI_ALLEGRETTO_RANK = KIMI_MEMBERSHIP_RANK.LEVEL_INTERMEDIATE;
|
|
74
|
+
|
|
75
|
+
export function isKimiModelAvailableForMembership(
|
|
76
|
+
modelId: string,
|
|
77
|
+
membershipLevel: string | null | undefined,
|
|
78
|
+
): boolean | undefined {
|
|
79
|
+
if (!membershipLevel) return undefined;
|
|
80
|
+
const rank = KIMI_MEMBERSHIP_RANK[membershipLevel];
|
|
81
|
+
if (rank === undefined) return undefined;
|
|
82
|
+
if (modelId === KIMI_K3_MODEL_ID) return rank >= KIMI_MODERATO_RANK;
|
|
83
|
+
if (modelId === KIMI_CODING_HIGHSPEED_MODEL_ID) return rank >= KIMI_ALLEGRETTO_RANK;
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function applyKimiMembershipLimitsToModel(
|
|
88
|
+
model: Model<Api>,
|
|
89
|
+
membershipLevel: string | null | undefined,
|
|
90
|
+
): Model<Api> {
|
|
91
|
+
const rank = membershipLevel ? KIMI_MEMBERSHIP_RANK[membershipLevel] : undefined;
|
|
92
|
+
if (model.id !== KIMI_K3_MODEL_ID || rank === undefined || rank >= KIMI_ALLEGRETTO_RANK) {
|
|
93
|
+
return model;
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
...model,
|
|
97
|
+
contextWindow: Math.min(model.contextWindow, KIMI_K3_MODERATO_CONTEXT_WINDOW),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
50
101
|
function resolveModelCost(modelDisplay: string | undefined): {
|
|
51
102
|
input: number;
|
|
52
103
|
output: number;
|
|
@@ -57,13 +108,23 @@ function resolveModelCost(modelDisplay: string | undefined): {
|
|
|
57
108
|
return COST_STANDARD;
|
|
58
109
|
}
|
|
59
110
|
|
|
60
|
-
export function buildKimiModelFromConfig(
|
|
111
|
+
export function buildKimiModelFromConfig(
|
|
112
|
+
config: ModelConfig,
|
|
113
|
+
modelId = KIMI_CODING_MODEL_ID,
|
|
114
|
+
): Model<Api> {
|
|
115
|
+
const isHighSpeed = modelId === KIMI_CODING_HIGHSPEED_MODEL_ID;
|
|
116
|
+
const name =
|
|
117
|
+
modelId === KIMI_K3_MODEL_ID
|
|
118
|
+
? "Kimi K3"
|
|
119
|
+
: isHighSpeed
|
|
120
|
+
? "Kimi for Coding High Speed"
|
|
121
|
+
: "Kimi for Coding";
|
|
61
122
|
return {
|
|
62
|
-
id:
|
|
63
|
-
name
|
|
123
|
+
id: modelId,
|
|
124
|
+
name,
|
|
64
125
|
reasoning: config.reasoning,
|
|
65
126
|
input: [...config.input] as unknown as ("text" | "image" | "video")[],
|
|
66
|
-
cost: { ...COST_STANDARD },
|
|
127
|
+
cost: { ...(isHighSpeed ? COST_HIGH_SPEED : COST_STANDARD) },
|
|
67
128
|
contextWindow: config.contextWindow,
|
|
68
129
|
maxTokens: config.maxTokens,
|
|
69
130
|
} as Model<Api>;
|
|
@@ -86,6 +147,8 @@ export function resolveKimiModelConfig(
|
|
|
86
147
|
if (typeof extras.supportsImageIn === "boolean" || typeof extras.supportsVideoIn === "boolean") {
|
|
87
148
|
resolved.input = mergeInputModalities(resolved.input, extras);
|
|
88
149
|
}
|
|
150
|
+
if (extras.supportEfforts) resolved.supportEfforts = [...extras.supportEfforts];
|
|
151
|
+
if (extras.defaultEffort) resolved.defaultEffort = extras.defaultEffort;
|
|
89
152
|
return resolved;
|
|
90
153
|
}
|
|
91
154
|
|
|
@@ -97,6 +160,8 @@ interface KimiServerModel {
|
|
|
97
160
|
supports_image_in?: unknown;
|
|
98
161
|
supports_video_in?: unknown;
|
|
99
162
|
supports_thinking_type?: unknown;
|
|
163
|
+
protocol?: unknown;
|
|
164
|
+
think_efforts?: unknown;
|
|
100
165
|
}
|
|
101
166
|
|
|
102
167
|
function parseSupportsThinkingType(value: unknown): "only" | "no" | "both" | undefined {
|
|
@@ -104,6 +169,54 @@ function parseSupportsThinkingType(value: unknown): "only" | "no" | "both" | und
|
|
|
104
169
|
return undefined;
|
|
105
170
|
}
|
|
106
171
|
|
|
172
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
173
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function parseThinkEfforts(value: unknown): {
|
|
177
|
+
supportEfforts?: string[];
|
|
178
|
+
defaultEffort?: string;
|
|
179
|
+
} {
|
|
180
|
+
if (!isRecord(value) || value.support !== true) return {};
|
|
181
|
+
const validEfforts = Array.isArray(value.valid_efforts)
|
|
182
|
+
? value.valid_efforts.filter(
|
|
183
|
+
(effort): effort is string => typeof effort === "string" && !!effort,
|
|
184
|
+
)
|
|
185
|
+
: [];
|
|
186
|
+
return {
|
|
187
|
+
...(validEfforts.length > 0 ? { supportEfforts: validEfforts } : {}),
|
|
188
|
+
...(typeof value.default_effort === "string" && value.default_effort
|
|
189
|
+
? { defaultEffort: value.default_effort }
|
|
190
|
+
: {}),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function parseKimiModelMetadata(model: KimiServerModel): KimiModelMetadata | undefined {
|
|
195
|
+
if (typeof model.id !== "string" || !model.id) return undefined;
|
|
196
|
+
const metadata: KimiModelMetadata = { wireModelId: model.id };
|
|
197
|
+
if (typeof model.display_name === "string") metadata.modelDisplay = model.display_name;
|
|
198
|
+
if (typeof model.context_length === "number" && model.context_length > 0) {
|
|
199
|
+
metadata.contextLength = model.context_length;
|
|
200
|
+
}
|
|
201
|
+
const thinkingType = parseSupportsThinkingType(model.supports_thinking_type);
|
|
202
|
+
if (thinkingType) {
|
|
203
|
+
metadata.supportsThinkingType = thinkingType;
|
|
204
|
+
} else if (typeof model.supports_reasoning === "boolean") {
|
|
205
|
+
metadata.supportsReasoning = model.supports_reasoning;
|
|
206
|
+
}
|
|
207
|
+
if (typeof model.supports_image_in === "boolean") {
|
|
208
|
+
metadata.supportsImageIn = model.supports_image_in;
|
|
209
|
+
}
|
|
210
|
+
if (typeof model.supports_video_in === "boolean") {
|
|
211
|
+
metadata.supportsVideoIn = model.supports_video_in;
|
|
212
|
+
}
|
|
213
|
+
if (model.protocol === "openai" || model.protocol === "anthropic") {
|
|
214
|
+
metadata.protocol = model.protocol;
|
|
215
|
+
}
|
|
216
|
+
Object.assign(metadata, parseThinkEfforts(model.think_efforts));
|
|
217
|
+
return metadata;
|
|
218
|
+
}
|
|
219
|
+
|
|
107
220
|
export function buildModelsUrl(baseUrl: string): string {
|
|
108
221
|
const base = baseUrl.replace(/\/+$/, "");
|
|
109
222
|
return base.endsWith("/v1") ? `${base}/models` : `${base}/v1/models`;
|
|
@@ -127,7 +240,7 @@ export async function discoverKimiModelMetadata(
|
|
|
127
240
|
const response = await fetch(getModelsUrl(protocol), {
|
|
128
241
|
signal: controller.signal,
|
|
129
242
|
headers: {
|
|
130
|
-
...
|
|
243
|
+
...getKimiProviderHeaders(),
|
|
131
244
|
Authorization: `Bearer ${accessToken}`,
|
|
132
245
|
Accept: "application/json",
|
|
133
246
|
},
|
|
@@ -135,26 +248,24 @@ export async function discoverKimiModelMetadata(
|
|
|
135
248
|
if (!response.ok) return {};
|
|
136
249
|
const json = (await response.json()) as { data?: unknown };
|
|
137
250
|
const list = Array.isArray(json.data) ? (json.data as KimiServerModel[]) : [];
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
} else if (typeof preferred.supports_reasoning === "boolean") {
|
|
149
|
-
extras.supportsReasoning = preferred.supports_reasoning;
|
|
251
|
+
const supportedIds = new Set([
|
|
252
|
+
KIMI_CODING_MODEL_ID,
|
|
253
|
+
KIMI_CODING_HIGHSPEED_MODEL_ID,
|
|
254
|
+
KIMI_K3_MODEL_ID,
|
|
255
|
+
]);
|
|
256
|
+
const modelCatalog: Record<string, KimiModelMetadata> = {};
|
|
257
|
+
for (const model of list) {
|
|
258
|
+
if (typeof model.id !== "string" || !supportedIds.has(model.id)) continue;
|
|
259
|
+
const metadata = parseKimiModelMetadata(model);
|
|
260
|
+
if (metadata) modelCatalog[model.id] = metadata;
|
|
150
261
|
}
|
|
151
|
-
|
|
152
|
-
|
|
262
|
+
const preferred = modelCatalog[KIMI_CODING_MODEL_ID];
|
|
263
|
+
if (!preferred) {
|
|
264
|
+
return Object.keys(modelCatalog).length > 0
|
|
265
|
+
? { modelCatalog, modelCatalogVersion: KIMI_MODEL_CATALOG_VERSION }
|
|
266
|
+
: {};
|
|
153
267
|
}
|
|
154
|
-
|
|
155
|
-
extras.supportsVideoIn = preferred.supports_video_in;
|
|
156
|
-
}
|
|
157
|
-
return extras;
|
|
268
|
+
return { ...preferred, modelCatalog, modelCatalogVersion: KIMI_MODEL_CATALOG_VERSION };
|
|
158
269
|
} catch {
|
|
159
270
|
return {};
|
|
160
271
|
} finally {
|
|
@@ -162,14 +273,28 @@ export async function discoverKimiModelMetadata(
|
|
|
162
273
|
}
|
|
163
274
|
}
|
|
164
275
|
|
|
276
|
+
export function getKimiModelMetadata(extras: KimiOAuthExtras, modelId: string): KimiModelMetadata {
|
|
277
|
+
const discovered = extras.modelCatalog?.[modelId];
|
|
278
|
+
if (discovered) return discovered;
|
|
279
|
+
return modelId === KIMI_CODING_MODEL_ID ? extras : {};
|
|
280
|
+
}
|
|
281
|
+
|
|
165
282
|
export function applyKimiOAuthExtrasToModel(
|
|
166
283
|
model: Model<Api>,
|
|
167
|
-
extras:
|
|
284
|
+
extras: KimiModelMetadata,
|
|
168
285
|
): Model<Api> {
|
|
169
|
-
const next: Model<Api> & {
|
|
170
|
-
|
|
286
|
+
const next: Model<Api> & {
|
|
287
|
+
wireModelId?: string;
|
|
288
|
+
supportsThinkingType?: "only" | "no" | "both";
|
|
289
|
+
wireProtocol?: KimiWireProtocol;
|
|
290
|
+
supportEfforts?: string[];
|
|
291
|
+
defaultEffort?: string;
|
|
292
|
+
} = { ...model };
|
|
171
293
|
if (typeof extras.modelDisplay === "string" && extras.modelDisplay) {
|
|
172
|
-
next.name =
|
|
294
|
+
next.name =
|
|
295
|
+
model.id === KIMI_K3_MODEL_ID && /^k3$/i.test(extras.modelDisplay)
|
|
296
|
+
? "Kimi K3"
|
|
297
|
+
: extras.modelDisplay;
|
|
173
298
|
next.cost = resolveModelCost(extras.modelDisplay);
|
|
174
299
|
}
|
|
175
300
|
if (typeof extras.contextLength === "number" && extras.contextLength > 0) {
|
|
@@ -189,5 +314,8 @@ export function applyKimiOAuthExtrasToModel(
|
|
|
189
314
|
const input = mergeInputModalities(next.input as KimiInputModality[], extras);
|
|
190
315
|
(next as unknown as { input: string[] }).input = input;
|
|
191
316
|
}
|
|
317
|
+
if (extras.protocol) next.wireProtocol = extras.protocol;
|
|
318
|
+
if (extras.supportEfforts) next.supportEfforts = [...extras.supportEfforts];
|
|
319
|
+
if (extras.defaultEffort) next.defaultEffort = extras.defaultEffort;
|
|
192
320
|
return next;
|
|
193
321
|
}
|
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";
|
|
@@ -120,11 +120,34 @@ export async function requestDeviceToken(
|
|
|
120
120
|
|
|
121
121
|
interface RefreshAccessTokenOptions {
|
|
122
122
|
maxRetries?: number;
|
|
123
|
+
signal?: AbortSignal;
|
|
123
124
|
sleep?: (ms: number) => Promise<void>;
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
export const KIMI_LOGIN_REQUIRED_MESSAGE =
|
|
128
|
+
"Kimi Code authorization is no longer valid. Sign in again with /login kimi-coding.";
|
|
129
|
+
|
|
130
|
+
export class KimiLoginRequiredError extends Error {
|
|
131
|
+
constructor() {
|
|
132
|
+
super(KIMI_LOGIN_REQUIRED_MESSAGE);
|
|
133
|
+
this.name = "KimiLoginRequiredError";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
138
|
+
if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
|
|
139
|
+
signal.throwIfAborted();
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
const onAbort = () => {
|
|
142
|
+
clearTimeout(timeout);
|
|
143
|
+
reject(signal.reason);
|
|
144
|
+
};
|
|
145
|
+
const timeout = setTimeout(() => {
|
|
146
|
+
signal.removeEventListener("abort", onAbort);
|
|
147
|
+
resolve();
|
|
148
|
+
}, ms);
|
|
149
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
150
|
+
});
|
|
128
151
|
}
|
|
129
152
|
|
|
130
153
|
export async function refreshAccessToken(
|
|
@@ -132,11 +155,12 @@ export async function refreshAccessToken(
|
|
|
132
155
|
options: RefreshAccessTokenOptions = {},
|
|
133
156
|
): Promise<TokenResponse> {
|
|
134
157
|
const maxRetries = options.maxRetries ?? 3;
|
|
135
|
-
const wait = options.sleep ?? sleep;
|
|
158
|
+
const wait = options.sleep ?? ((ms: number) => sleep(ms, options.signal));
|
|
136
159
|
let lastError: unknown;
|
|
137
160
|
|
|
138
161
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
139
162
|
try {
|
|
163
|
+
options.signal?.throwIfAborted();
|
|
140
164
|
const response = await fetch(`${getOAuthHost()}/api/oauth/token`, {
|
|
141
165
|
method: "POST",
|
|
142
166
|
headers: {
|
|
@@ -148,12 +172,20 @@ export async function refreshAccessToken(
|
|
|
148
172
|
grant_type: "refresh_token",
|
|
149
173
|
refresh_token: refreshToken,
|
|
150
174
|
}),
|
|
175
|
+
signal: options.signal,
|
|
151
176
|
});
|
|
152
177
|
|
|
153
178
|
if (!response.ok) {
|
|
154
179
|
const text = await response.text().catch(() => "");
|
|
155
|
-
|
|
156
|
-
|
|
180
|
+
let errorCode = "";
|
|
181
|
+
try {
|
|
182
|
+
const body = JSON.parse(text) as { error?: unknown };
|
|
183
|
+
if (typeof body.error === "string") errorCode = body.error;
|
|
184
|
+
} catch {
|
|
185
|
+
// Non-JSON OAuth errors are classified by status below.
|
|
186
|
+
}
|
|
187
|
+
if (response.status === 401 || response.status === 403 || errorCode === "invalid_grant") {
|
|
188
|
+
throw new KimiLoginRequiredError();
|
|
157
189
|
}
|
|
158
190
|
if (RETRYABLE_REFRESH_STATUSES.has(response.status)) {
|
|
159
191
|
throw new Error(`Token refresh retryable: ${response.status} ${text}`);
|
|
@@ -168,9 +200,10 @@ export async function refreshAccessToken(
|
|
|
168
200
|
|
|
169
201
|
return data;
|
|
170
202
|
} catch (error) {
|
|
203
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
171
204
|
lastError = error;
|
|
172
205
|
const message = error instanceof Error ? error.message : String(error);
|
|
173
|
-
if (
|
|
206
|
+
if (error instanceof KimiLoginRequiredError) throw error;
|
|
174
207
|
if (message.startsWith("Token refresh failed:")) throw error;
|
|
175
208
|
if (attempt < maxRetries - 1) {
|
|
176
209
|
await wait(2 ** attempt * 1000);
|
|
@@ -196,15 +229,19 @@ interface KimiCliCredentialsFile {
|
|
|
196
229
|
access_token?: string;
|
|
197
230
|
refresh_token?: string;
|
|
198
231
|
expires_at?: number; // Unix seconds (upstream convention)
|
|
232
|
+
scope?: string;
|
|
233
|
+
token_type?: string;
|
|
234
|
+
expires_in?: number;
|
|
199
235
|
}
|
|
200
236
|
|
|
201
|
-
function
|
|
237
|
+
function getKimiCodeCredentialPath(): string {
|
|
202
238
|
const kimiCodeHome = process.env.KIMI_CODE_HOME || join(os.homedir(), ".kimi-code");
|
|
239
|
+
return join(kimiCodeHome, "credentials", "kimi-code.json");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getKimiCredentialPaths(): string[] {
|
|
203
243
|
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
|
-
];
|
|
244
|
+
return [getKimiCodeCredentialPath(), join(shareDir, "credentials", "kimi-code.json")];
|
|
208
245
|
}
|
|
209
246
|
|
|
210
247
|
function readKimiCliCredentials(): KimiCliCredentialsFile | null {
|
|
@@ -221,6 +258,25 @@ function readKimiCliCredentials(): KimiCliCredentialsFile | null {
|
|
|
221
258
|
return null;
|
|
222
259
|
}
|
|
223
260
|
|
|
261
|
+
function kimiCodeCredentialExists(): boolean {
|
|
262
|
+
return existsSync(getKimiCodeCredentialPath());
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function writeKimiCodeCredentials(access: string, refresh: string, expiresMs: number): void {
|
|
266
|
+
const path = getKimiCodeCredentialPath();
|
|
267
|
+
const dir = dirname(path);
|
|
268
|
+
mkdirSync(dir, { recursive: true });
|
|
269
|
+
const data: KimiCliCredentialsFile = {
|
|
270
|
+
access_token: access,
|
|
271
|
+
refresh_token: refresh,
|
|
272
|
+
expires_at: Math.floor(expiresMs / 1000),
|
|
273
|
+
scope: "",
|
|
274
|
+
token_type: "Bearer",
|
|
275
|
+
expires_in: Math.max(0, Math.floor((expiresMs - Date.now()) / 1000)),
|
|
276
|
+
};
|
|
277
|
+
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
|
|
278
|
+
}
|
|
279
|
+
|
|
224
280
|
async function tryReuseKimiCliCredentials(
|
|
225
281
|
callbacks: OAuthLoginCallbacks,
|
|
226
282
|
): Promise<KimiOAuthCredentials | null> {
|
|
@@ -247,11 +303,13 @@ async function tryReuseKimiCliCredentials(
|
|
|
247
303
|
callbacks.onProgress?.("Kimi access token expired, refreshing.");
|
|
248
304
|
try {
|
|
249
305
|
const token = await refreshAccessToken(data.refresh_token!);
|
|
306
|
+
const expiresMs = Date.now() + token.expires_in * 1000;
|
|
307
|
+
writeKimiCodeCredentials(token.access_token, token.refresh_token, expiresMs);
|
|
250
308
|
const extras = await discoverKimiModelMetadata(token.access_token);
|
|
251
309
|
return {
|
|
252
310
|
access: token.access_token,
|
|
253
311
|
refresh: token.refresh_token,
|
|
254
|
-
expires:
|
|
312
|
+
expires: expiresMs,
|
|
255
313
|
...extras,
|
|
256
314
|
};
|
|
257
315
|
} catch {
|
|
@@ -319,11 +377,13 @@ export async function loginKimiCode(callbacks: OAuthLoginCallbacks): Promise<Kim
|
|
|
319
377
|
}
|
|
320
378
|
|
|
321
379
|
if (token) {
|
|
380
|
+
const expiresMs = Date.now() + token.expires_in * 1000;
|
|
381
|
+
writeKimiCodeCredentials(token.access_token, token.refresh_token, expiresMs);
|
|
322
382
|
const extras = await discoverKimiModelMetadata(token.access_token);
|
|
323
383
|
return {
|
|
324
384
|
access: token.access_token,
|
|
325
385
|
refresh: token.refresh_token,
|
|
326
|
-
expires:
|
|
386
|
+
expires: expiresMs,
|
|
327
387
|
...extras,
|
|
328
388
|
};
|
|
329
389
|
}
|
|
@@ -335,12 +395,24 @@ export async function loginKimiCode(callbacks: OAuthLoginCallbacks): Promise<Kim
|
|
|
335
395
|
export async function refreshKimiCodeToken(
|
|
336
396
|
credentials: OAuthCredentials,
|
|
337
397
|
): Promise<KimiOAuthCredentials> {
|
|
338
|
-
const
|
|
398
|
+
const kimiCred = kimiCodeCredentialExists() ? readKimiCliCredentials() : null;
|
|
399
|
+
let token: Awaited<ReturnType<typeof refreshAccessToken>>;
|
|
400
|
+
try {
|
|
401
|
+
token = await refreshAccessToken(credentials.refresh);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (kimiCred?.refresh_token && kimiCred.refresh_token !== credentials.refresh) {
|
|
404
|
+
token = await refreshAccessToken(kimiCred.refresh_token);
|
|
405
|
+
} else {
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const expiresMs = Date.now() + token.expires_in * 1000;
|
|
410
|
+
writeKimiCodeCredentials(token.access_token, token.refresh_token, expiresMs);
|
|
339
411
|
const extras = await discoverKimiModelMetadata(token.access_token);
|
|
340
412
|
return {
|
|
341
413
|
access: token.access_token,
|
|
342
414
|
refresh: token.refresh_token,
|
|
343
|
-
expires:
|
|
415
|
+
expires: expiresMs,
|
|
344
416
|
...extras,
|
|
345
417
|
};
|
|
346
418
|
}
|
|
@@ -356,8 +428,17 @@ export async function refreshKimiCodeToken(
|
|
|
356
428
|
// new credentials under a file lock), and retry once.
|
|
357
429
|
// =============================================================================
|
|
358
430
|
|
|
431
|
+
export function getKimiApiKey(credentials: OAuthCredentials): string {
|
|
432
|
+
return credentials.access;
|
|
433
|
+
}
|
|
434
|
+
|
|
359
435
|
export function isKimiAuthErrorMessage(message: unknown): boolean {
|
|
360
436
|
const text = String(message ?? "").toLowerCase();
|
|
437
|
+
const isMembershipPermissionError =
|
|
438
|
+
text.includes("current subscription does not have access to k3") ||
|
|
439
|
+
text.includes("current plan supports only kimi-k3 up to 256k context") ||
|
|
440
|
+
text.includes("current subscription does not have access to kimi-for-coding-highspeed");
|
|
441
|
+
if (isMembershipPermissionError) return false;
|
|
361
442
|
return (
|
|
362
443
|
/\b401\b/.test(text) ||
|
|
363
444
|
text.includes("unauthorized") ||
|
|
@@ -366,39 +447,87 @@ export function isKimiAuthErrorMessage(message: unknown): boolean {
|
|
|
366
447
|
);
|
|
367
448
|
}
|
|
368
449
|
|
|
369
|
-
export async function refreshKimiAuthToken(
|
|
450
|
+
export async function refreshKimiAuthToken(
|
|
451
|
+
currentKey: string,
|
|
452
|
+
options: { signal?: AbortSignal } = {},
|
|
453
|
+
): Promise<string | null> {
|
|
370
454
|
try {
|
|
455
|
+
const kimiCred = readKimiCliCredentials();
|
|
371
456
|
const storage = AuthStorage.create();
|
|
372
|
-
const
|
|
373
|
-
|
|
457
|
+
const piCred = storage.get(PROVIDER_ID);
|
|
458
|
+
const piOAuth = piCred?.type === "oauth" ? piCred : null;
|
|
459
|
+
|
|
460
|
+
if (piOAuth) {
|
|
461
|
+
if (piOAuth.access !== currentKey && Date.now() < piOAuth.expires) {
|
|
462
|
+
console.error("[kimi-coding] auth refresh: reusing newer on-disk token");
|
|
463
|
+
return piOAuth.access;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
console.error("[kimi-coding] auth refresh: requesting new access token");
|
|
467
|
+
let refreshed: Awaited<ReturnType<typeof refreshAccessToken>>;
|
|
468
|
+
try {
|
|
469
|
+
refreshed = await refreshAccessToken(piOAuth.refresh, { signal: options.signal });
|
|
470
|
+
} catch (error) {
|
|
471
|
+
if (!kimiCred?.refresh_token || kimiCred.refresh_token === piOAuth.refresh) throw error;
|
|
472
|
+
const kimiExpiresMs = (kimiCred.expires_at ?? 0) * 1000;
|
|
473
|
+
if (kimiCred.access_token !== currentKey && Date.now() < kimiExpiresMs) {
|
|
474
|
+
const recovered: OAuthCredential = {
|
|
475
|
+
...piOAuth,
|
|
476
|
+
type: "oauth",
|
|
477
|
+
access: kimiCred.access_token!,
|
|
478
|
+
refresh: kimiCred.refresh_token,
|
|
479
|
+
expires: kimiExpiresMs,
|
|
480
|
+
};
|
|
481
|
+
storage.set(PROVIDER_ID, recovered);
|
|
482
|
+
console.error("[kimi-coding] auth refresh: recovered newer kimi-code token");
|
|
483
|
+
return recovered.access;
|
|
484
|
+
}
|
|
485
|
+
console.error("[kimi-coding] auth refresh: pi token rejected, trying kimi-code token");
|
|
486
|
+
refreshed = await refreshAccessToken(kimiCred.refresh_token, { signal: options.signal });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
|
|
490
|
+
const newCred: OAuthCredential = {
|
|
491
|
+
...piOAuth,
|
|
492
|
+
type: "oauth",
|
|
493
|
+
access: refreshed.access_token,
|
|
494
|
+
refresh: refreshed.refresh_token,
|
|
495
|
+
expires: newExpiresMs,
|
|
496
|
+
};
|
|
497
|
+
storage.set(PROVIDER_ID, newCred);
|
|
498
|
+
writeKimiCodeCredentials(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
|
|
499
|
+
console.error("[kimi-coding] auth refresh: new token persisted");
|
|
500
|
+
return newCred.access;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (!kimiCred) {
|
|
374
504
|
console.error(
|
|
375
505
|
`[kimi-coding] auth refresh skipped: no OAuth credentials for ${PROVIDER_ID} on disk`,
|
|
376
506
|
);
|
|
377
507
|
return null;
|
|
378
508
|
}
|
|
379
509
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
console.error("[kimi-coding] auth refresh: reusing newer on-disk token");
|
|
385
|
-
return cred.access;
|
|
510
|
+
const kimiExpiresMs = (kimiCred.expires_at ?? 0) * 1000;
|
|
511
|
+
if (kimiCred.access_token !== currentKey && Date.now() < kimiExpiresMs) {
|
|
512
|
+
console.error("[kimi-coding] auth refresh: reusing newer kimi-code token");
|
|
513
|
+
return kimiCred.access_token!;
|
|
386
514
|
}
|
|
387
515
|
|
|
388
|
-
console.error("[kimi-coding] auth refresh: requesting new access token");
|
|
389
|
-
const refreshed = await refreshAccessToken(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
};
|
|
397
|
-
storage.set(PROVIDER_ID, newCred);
|
|
398
|
-
console.error("[kimi-coding] auth refresh: new token persisted");
|
|
399
|
-
return newCred.access;
|
|
516
|
+
console.error("[kimi-coding] auth refresh: requesting new access token via kimi-code");
|
|
517
|
+
const refreshed = await refreshAccessToken(kimiCred.refresh_token!, {
|
|
518
|
+
signal: options.signal,
|
|
519
|
+
});
|
|
520
|
+
const newExpiresMs = Date.now() + refreshed.expires_in * 1000;
|
|
521
|
+
writeKimiCodeCredentials(refreshed.access_token, refreshed.refresh_token, newExpiresMs);
|
|
522
|
+
console.error("[kimi-coding] auth refresh: new token persisted to kimi-code");
|
|
523
|
+
return refreshed.access_token;
|
|
400
524
|
} catch (err) {
|
|
401
|
-
|
|
525
|
+
if (options.signal?.aborted) return null;
|
|
526
|
+
if (err instanceof KimiLoginRequiredError) {
|
|
527
|
+
console.error(`[kimi-coding] ${KIMI_LOGIN_REQUIRED_MESSAGE}`);
|
|
528
|
+
} else {
|
|
529
|
+
console.error("[kimi-coding] auth refresh failed:", err);
|
|
530
|
+
}
|
|
402
531
|
return null;
|
|
403
532
|
}
|
|
404
533
|
}
|