pi-provider-kimi-code 0.6.7 → 0.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -109
- package/index.ts +231 -87
- package/package.json +5 -5
- package/src/constants.ts +6 -2
- package/src/device.ts +59 -17
- package/src/models.ts +147 -62
- package/src/oauth.ts +232 -68
- package/src/payload.ts +154 -40
- package/src/stream.ts +32 -4
- package/src/usage.ts +4 -42
package/src/models.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
// Model identity: discovery against the server's /v1/models endpoint, plus the
|
|
2
2
|
// extras-merging helpers used by both registration and the OAuth modifyModels hook.
|
|
3
3
|
|
|
4
|
-
import type { Api, Model, OAuthCredentials } from "@earendil-works/pi-ai";
|
|
5
|
-
import type {
|
|
4
|
+
import type { Api, Model, OAuthCredentials, ThinkingLevelMap } from "@earendil-works/pi-ai";
|
|
5
|
+
import type {
|
|
6
|
+
KimiInputModality,
|
|
7
|
+
KimiResolvedModelConfig,
|
|
8
|
+
ModelConfig,
|
|
9
|
+
ModelReasoningMap,
|
|
10
|
+
} from "./config.ts";
|
|
11
|
+
import { DEFAULT_KIMI_CODE_CONFIG } from "./config.ts";
|
|
6
12
|
|
|
7
13
|
import { type KimiWireProtocol, getBaseUrl } from "./constants.ts";
|
|
8
14
|
import { getKimiProviderHeaders } from "./device.ts";
|
|
@@ -31,6 +37,10 @@ const DEFAULT_DISCOVERY_TIMEOUT_MS = 2500;
|
|
|
31
37
|
|
|
32
38
|
export interface DiscoverKimiModelMetadataOptions {
|
|
33
39
|
timeoutMs?: number;
|
|
40
|
+
// 401 recovery for runtime callers (extension startup, settings menu).
|
|
41
|
+
// oauth.ts's login/refresh flows must NOT pass this: they call discovery
|
|
42
|
+
// while holding the OAuth lock, and refreshKimiAuthToken would re-enter it.
|
|
43
|
+
refreshAccessToken?: (currentToken: string) => Promise<string | null>;
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
function mergeInputModalities(
|
|
@@ -50,62 +60,63 @@ function mergeInputModalities(
|
|
|
50
60
|
return (["text", "image", "video"] as const).filter((modality) => next.has(modality));
|
|
51
61
|
}
|
|
52
62
|
|
|
53
|
-
// Pricing per million tokens in USD
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
const
|
|
63
|
+
// Pricing per million tokens in USD.
|
|
64
|
+
// Sources: https://www.kimi.com/resources/kimi-k2-7-code-pricing
|
|
65
|
+
// https://www.kimi.com/resources/kimi-k3-pricing
|
|
66
|
+
const COST_STANDARD = { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0.95 };
|
|
67
|
+
const COST_HIGH_SPEED = { input: 1.9, output: 8, cacheRead: 0.38, cacheWrite: 1.9 };
|
|
68
|
+
const COST_K3 = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3 };
|
|
57
69
|
|
|
58
70
|
export const KIMI_CODING_MODEL_ID = "kimi-for-coding";
|
|
59
71
|
export const KIMI_CODING_HIGHSPEED_MODEL_ID = "kimi-for-coding-highspeed";
|
|
60
72
|
export const KIMI_K3_MODEL_ID = "k3";
|
|
61
73
|
export const KIMI_MODEL_CATALOG_VERSION = 1;
|
|
62
74
|
|
|
63
|
-
|
|
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(
|
|
75
|
+
function resolveModelCost(
|
|
76
76
|
modelId: string,
|
|
77
|
-
|
|
78
|
-
):
|
|
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
|
-
|
|
101
|
-
function resolveModelCost(modelDisplay: string | undefined): {
|
|
77
|
+
modelDisplay?: string,
|
|
78
|
+
): {
|
|
102
79
|
input: number;
|
|
103
80
|
output: number;
|
|
104
81
|
cacheRead: number;
|
|
105
82
|
cacheWrite: number;
|
|
106
83
|
} {
|
|
107
|
-
if (
|
|
108
|
-
return COST_STANDARD;
|
|
84
|
+
if (modelId === KIMI_K3_MODEL_ID) return COST_K3;
|
|
85
|
+
if (modelId === KIMI_CODING_MODEL_ID) return COST_STANDARD;
|
|
86
|
+
if (modelId === KIMI_CODING_HIGHSPEED_MODEL_ID || /high\s*speed/i.test(modelDisplay ?? "")) {
|
|
87
|
+
return COST_HIGH_SPEED;
|
|
88
|
+
}
|
|
89
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Policy: a model's output cap tracks its context window unless the user
|
|
93
|
+
// explicitly configured model.maxTokens. pi-ai clamps the request cap to
|
|
94
|
+
// contextWindow - used - 4096 (clampMaxTokensToContext in
|
|
95
|
+
// @earendil-works/pi-ai), so setting maxTokens to the window yields the official
|
|
96
|
+
// kimi-code behavior (computeCompletionBudgetCap: maxCtx, clamped to
|
|
97
|
+
// maxCtx - usedContext) instead of a fixed small cap that truncates long
|
|
98
|
+
// max-effort reasoning (reasoning_content counts toward the cap). A deliberate
|
|
99
|
+
// per-request cap still wins (options.maxTokens ?? model.maxTokens). Reconcile
|
|
100
|
+
// wherever contextWindow is authored so the two stay in lockstep.
|
|
101
|
+
//
|
|
102
|
+
// "Explicitly configured" means any value other than the built-in default:
|
|
103
|
+
// ensureKimiCodeConfig materializes the full default config (including
|
|
104
|
+
// maxTokens) into the home config file, so config-source tracking cannot
|
|
105
|
+
// distinguish a deliberate 32000 from a materialized default. A user who
|
|
106
|
+
// explicitly sets the default value gets the window cap — indistinguishable
|
|
107
|
+
// from the materialized-default case, and the intended new behavior anyway.
|
|
108
|
+
const DEFAULT_MODEL_MAX_TOKENS = DEFAULT_KIMI_CODE_CONFIG.model.maxTokens;
|
|
109
|
+
|
|
110
|
+
// True while the cap is "unset": still the built-in default, or already
|
|
111
|
+
// tracking the context window (set by an earlier withWindowCappedMaxTokens
|
|
112
|
+
// call, before discovery grew the window).
|
|
113
|
+
function isWindowTrackedMaxTokens(model: Model<Api>): boolean {
|
|
114
|
+
return model.maxTokens === DEFAULT_MODEL_MAX_TOKENS || model.maxTokens === model.contextWindow;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function withWindowCappedMaxTokens<M extends Model<Api>>(model: M): M {
|
|
118
|
+
if (!isWindowTrackedMaxTokens(model)) return model;
|
|
119
|
+
return { ...model, maxTokens: model.contextWindow };
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
export function buildKimiModelFromConfig(
|
|
@@ -118,16 +129,18 @@ export function buildKimiModelFromConfig(
|
|
|
118
129
|
? "Kimi K3"
|
|
119
130
|
: isHighSpeed
|
|
120
131
|
? "Kimi for Coding High Speed"
|
|
121
|
-
:
|
|
122
|
-
|
|
132
|
+
: modelId === KIMI_CODING_MODEL_ID
|
|
133
|
+
? "Kimi for Coding"
|
|
134
|
+
: modelId;
|
|
135
|
+
return withWindowCappedMaxTokens({
|
|
123
136
|
id: modelId,
|
|
124
137
|
name,
|
|
125
138
|
reasoning: config.reasoning,
|
|
126
139
|
input: [...config.input] as unknown as ("text" | "image" | "video")[],
|
|
127
|
-
cost: { ...(
|
|
140
|
+
cost: { ...resolveModelCost(modelId) },
|
|
128
141
|
contextWindow: config.contextWindow,
|
|
129
142
|
maxTokens: config.maxTokens,
|
|
130
|
-
} as Model<Api
|
|
143
|
+
} as Model<Api>);
|
|
131
144
|
}
|
|
132
145
|
|
|
133
146
|
export function resolveKimiModelConfig(
|
|
@@ -226,38 +239,58 @@ function getModelsUrl(protocol?: KimiWireProtocol): string {
|
|
|
226
239
|
return buildModelsUrl(getBaseUrl(protocol));
|
|
227
240
|
}
|
|
228
241
|
|
|
242
|
+
export function isOfficialKimiModelsUrl(url: string): boolean {
|
|
243
|
+
try {
|
|
244
|
+
const parsed = new URL(url);
|
|
245
|
+
return parsed.origin === "https://api.kimi.com" && parsed.pathname === "/coding/v1/models";
|
|
246
|
+
} catch {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
229
251
|
export async function discoverKimiModelMetadata(
|
|
230
252
|
accessToken: string,
|
|
231
253
|
protocol?: KimiWireProtocol,
|
|
232
254
|
options: DiscoverKimiModelMetadataOptions = {},
|
|
233
255
|
): Promise<KimiOAuthExtras> {
|
|
234
256
|
if (!accessToken) return {};
|
|
257
|
+
const modelsUrl = getModelsUrl(protocol);
|
|
258
|
+
if (!isOfficialKimiModelsUrl(modelsUrl)) return {};
|
|
235
259
|
const timeoutMs = options.timeoutMs ?? DEFAULT_DISCOVERY_TIMEOUT_MS;
|
|
236
260
|
const controller = new AbortController();
|
|
237
261
|
const timeout =
|
|
238
262
|
timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs).unref() : undefined;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
signal
|
|
263
|
+
const requestModels = (token: string, signal: AbortSignal) =>
|
|
264
|
+
fetch(modelsUrl, {
|
|
265
|
+
signal,
|
|
242
266
|
headers: {
|
|
243
267
|
...getKimiProviderHeaders(),
|
|
244
|
-
Authorization: `Bearer ${
|
|
268
|
+
Authorization: `Bearer ${token}`,
|
|
245
269
|
Accept: "application/json",
|
|
246
270
|
},
|
|
247
271
|
});
|
|
272
|
+
try {
|
|
273
|
+
let response = await requestModels(accessToken, controller.signal);
|
|
274
|
+
if (response.status === 401 && options.refreshAccessToken) {
|
|
275
|
+
const refreshed = await options.refreshAccessToken(accessToken);
|
|
276
|
+
if (refreshed && refreshed !== accessToken) {
|
|
277
|
+
const retryController = new AbortController();
|
|
278
|
+
const retryTimeout =
|
|
279
|
+
timeoutMs > 0 ? setTimeout(() => retryController.abort(), timeoutMs).unref() : undefined;
|
|
280
|
+
try {
|
|
281
|
+
response = await requestModels(refreshed, retryController.signal);
|
|
282
|
+
} finally {
|
|
283
|
+
if (retryTimeout) clearTimeout(retryTimeout);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
248
287
|
if (!response.ok) return {};
|
|
249
288
|
const json = (await response.json()) as { data?: unknown };
|
|
250
289
|
const list = Array.isArray(json.data) ? (json.data as KimiServerModel[]) : [];
|
|
251
|
-
const supportedIds = new Set([
|
|
252
|
-
KIMI_CODING_MODEL_ID,
|
|
253
|
-
KIMI_CODING_HIGHSPEED_MODEL_ID,
|
|
254
|
-
KIMI_K3_MODEL_ID,
|
|
255
|
-
]);
|
|
256
290
|
const modelCatalog: Record<string, KimiModelMetadata> = {};
|
|
257
291
|
for (const model of list) {
|
|
258
|
-
if (typeof model.id !== "string" || !supportedIds.has(model.id)) continue;
|
|
259
292
|
const metadata = parseKimiModelMetadata(model);
|
|
260
|
-
if (metadata) modelCatalog[
|
|
293
|
+
if (metadata) modelCatalog[metadata.wireModelId!] = metadata;
|
|
261
294
|
}
|
|
262
295
|
const preferred = modelCatalog[KIMI_CODING_MODEL_ID];
|
|
263
296
|
if (!preferred) {
|
|
@@ -279,10 +312,53 @@ export function getKimiModelMetadata(extras: KimiOAuthExtras, modelId: string):
|
|
|
279
312
|
return modelId === KIMI_CODING_MODEL_ID ? extras : {};
|
|
280
313
|
}
|
|
281
314
|
|
|
315
|
+
const KIMI_DISCOVERY_KEYS = [
|
|
316
|
+
"wireModelId",
|
|
317
|
+
"modelDisplay",
|
|
318
|
+
"contextLength",
|
|
319
|
+
"supportsReasoning",
|
|
320
|
+
"supportsImageIn",
|
|
321
|
+
"supportsVideoIn",
|
|
322
|
+
"supportsThinkingType",
|
|
323
|
+
"protocol",
|
|
324
|
+
"supportEfforts",
|
|
325
|
+
"defaultEffort",
|
|
326
|
+
"modelCatalog",
|
|
327
|
+
] as const;
|
|
328
|
+
|
|
329
|
+
// Credentials always carry access/refresh/expires, so discovery presence must
|
|
330
|
+
// be checked on the metadata fields themselves. Empty means either a legacy
|
|
331
|
+
// pre-discovery credential or a failed discovery during login/refresh.
|
|
332
|
+
export function hasKimiModelMetadata(extras: KimiOAuthExtras): boolean {
|
|
333
|
+
return KIMI_DISCOVERY_KEYS.some((key) => extras[key] !== undefined);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function buildKimiThinkingLevelMap(
|
|
337
|
+
reasoningMap: ModelReasoningMap,
|
|
338
|
+
extras: Pick<KimiModelMetadata, "supportEfforts" | "supportsThinkingType">,
|
|
339
|
+
): ThinkingLevelMap | undefined {
|
|
340
|
+
if (!extras.supportEfforts?.length) return undefined;
|
|
341
|
+
const map = {
|
|
342
|
+
off: extras.supportsThinkingType === "only" ? null : "off",
|
|
343
|
+
} as ThinkingLevelMap & Record<string, string | null>;
|
|
344
|
+
for (const level of ["minimal", "low", "medium", "high", "xhigh", "max"] as const) {
|
|
345
|
+
const entry = reasoningMap[level];
|
|
346
|
+
map[level] =
|
|
347
|
+
entry?.enabled && entry.effort && extras.supportEfforts.includes(entry.effort)
|
|
348
|
+
? entry.effort
|
|
349
|
+
: null;
|
|
350
|
+
}
|
|
351
|
+
return map;
|
|
352
|
+
}
|
|
353
|
+
|
|
282
354
|
export function applyKimiOAuthExtrasToModel(
|
|
283
355
|
model: Model<Api>,
|
|
284
356
|
extras: KimiModelMetadata,
|
|
357
|
+
reasoningMap?: ModelReasoningMap,
|
|
285
358
|
): Model<Api> {
|
|
359
|
+
// Capture before extras grow the window: a window-tracked cap must follow
|
|
360
|
+
// the new window, an explicit cap must survive unchanged.
|
|
361
|
+
const windowTracked = isWindowTrackedMaxTokens(model);
|
|
286
362
|
const next: Model<Api> & {
|
|
287
363
|
wireModelId?: string;
|
|
288
364
|
supportsThinkingType?: "only" | "no" | "both";
|
|
@@ -295,7 +371,7 @@ export function applyKimiOAuthExtrasToModel(
|
|
|
295
371
|
model.id === KIMI_K3_MODEL_ID && /^k3$/i.test(extras.modelDisplay)
|
|
296
372
|
? "Kimi K3"
|
|
297
373
|
: extras.modelDisplay;
|
|
298
|
-
next.cost = resolveModelCost(extras.modelDisplay);
|
|
374
|
+
next.cost = resolveModelCost(model.id, extras.modelDisplay);
|
|
299
375
|
}
|
|
300
376
|
if (typeof extras.contextLength === "number" && extras.contextLength > 0) {
|
|
301
377
|
next.contextWindow = extras.contextLength;
|
|
@@ -316,6 +392,15 @@ export function applyKimiOAuthExtrasToModel(
|
|
|
316
392
|
}
|
|
317
393
|
if (extras.protocol) next.wireProtocol = extras.protocol;
|
|
318
394
|
if (extras.supportEfforts) next.supportEfforts = [...extras.supportEfforts];
|
|
395
|
+
else delete next.supportEfforts;
|
|
319
396
|
if (extras.defaultEffort) next.defaultEffort = extras.defaultEffort;
|
|
397
|
+
else delete next.defaultEffort;
|
|
398
|
+
if (reasoningMap) {
|
|
399
|
+
const thinkingLevelMap = buildKimiThinkingLevelMap(reasoningMap, extras);
|
|
400
|
+
if (thinkingLevelMap) next.thinkingLevelMap = thinkingLevelMap;
|
|
401
|
+
else delete next.thinkingLevelMap;
|
|
402
|
+
}
|
|
403
|
+
// Reconcile the output cap with the (possibly discovery-updated) window.
|
|
404
|
+
if (windowTracked) next.maxTokens = next.contextWindow;
|
|
320
405
|
return next;
|
|
321
406
|
}
|