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/payload.ts
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
// per-protocol message transforms, OpenAI tool-call / tool-schema normalizers,
|
|
3
3
|
// and the top-level applyKimiPayloadMutations that orchestrates them.
|
|
4
4
|
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
|
|
5
7
|
import type { CacheRetention, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
6
8
|
import type { KimiResolvedModelConfig, ModelReasoningEntry } from "./config.ts";
|
|
7
9
|
|
|
8
10
|
import { getBaseUrl } from "./constants.ts";
|
|
9
11
|
import { getKimiProviderHeaders } from "./device.ts";
|
|
12
|
+
import { refreshKimiAuthToken } from "./oauth.ts";
|
|
10
13
|
import { optimizeToolSchemas } from "./schema-dedup.ts";
|
|
11
14
|
|
|
12
15
|
// =============================================================================
|
|
@@ -30,6 +33,7 @@ export function resolveCacheRetention(value?: CacheRetention): CacheRetention {
|
|
|
30
33
|
export interface KimiPayloadContext {
|
|
31
34
|
api: "anthropic-messages" | "openai-completions";
|
|
32
35
|
upload?: Uploader;
|
|
36
|
+
uploadCacheScope?: string;
|
|
33
37
|
cacheKey?: string;
|
|
34
38
|
cacheRetention: CacheRetention;
|
|
35
39
|
reasoning?: ThinkingLevel;
|
|
@@ -80,6 +84,16 @@ function getUploadFilename(mimeType: string): string {
|
|
|
80
84
|
"image/png": "upload.png",
|
|
81
85
|
"image/gif": "upload.gif",
|
|
82
86
|
"image/webp": "upload.webp",
|
|
87
|
+
// Video extensions mirror upstream kimi-code's MIME_TO_EXT
|
|
88
|
+
// (packages/kosong/src/providers/kimi-files.ts).
|
|
89
|
+
"video/mp4": "upload.mp4",
|
|
90
|
+
"video/mpeg": "upload.mpeg",
|
|
91
|
+
"video/quicktime": "upload.mov",
|
|
92
|
+
"video/webm": "upload.webm",
|
|
93
|
+
"video/x-matroska": "upload.mkv",
|
|
94
|
+
"video/x-msvideo": "upload.avi",
|
|
95
|
+
"video/x-flv": "upload.flv",
|
|
96
|
+
"video/3gpp": "upload.3gp",
|
|
83
97
|
};
|
|
84
98
|
return map[mimeType] ?? "upload.bin";
|
|
85
99
|
}
|
|
@@ -88,22 +102,34 @@ function getUploadFilename(mimeType: string): string {
|
|
|
88
102
|
// File upload (I/O edge)
|
|
89
103
|
// =============================================================================
|
|
90
104
|
|
|
105
|
+
export interface UploadKimiFileDeps {
|
|
106
|
+
fetch?: typeof fetch;
|
|
107
|
+
refreshAccessToken?: (currentToken: string) => Promise<string | null>;
|
|
108
|
+
}
|
|
109
|
+
|
|
91
110
|
export async function uploadKimiFile(
|
|
92
111
|
apiKey: string,
|
|
93
112
|
mimeType: string,
|
|
94
113
|
data: string,
|
|
95
114
|
thresholdBytes?: number,
|
|
115
|
+
deps?: UploadKimiFileDeps,
|
|
96
116
|
): Promise<string | null> {
|
|
117
|
+
const fetchImpl = deps?.fetch ?? fetch;
|
|
118
|
+
const refreshAccessToken = deps?.refreshAccessToken ?? refreshKimiAuthToken;
|
|
97
119
|
const buffer = Buffer.from(data, "base64");
|
|
98
|
-
|
|
120
|
+
const isVideo = mimeType.startsWith("video/");
|
|
121
|
+
if (!mimeType.startsWith("image/") && !isVideo) return null;
|
|
99
122
|
const threshold =
|
|
100
123
|
thresholdBytes ?? parseInlineUploadThreshold(process.env.KIMI_CODE_UPLOAD_THRESHOLD_BYTES);
|
|
101
|
-
|
|
124
|
+
// The inline threshold applies to images only: the Kimi API has no inline
|
|
125
|
+
// video path (upstream kimi-code uploads every video via /files), so videos
|
|
126
|
+
// always upload.
|
|
127
|
+
if (!isVideo && buffer.length <= threshold) return null;
|
|
102
128
|
|
|
103
129
|
const filename = getUploadFilename(mimeType);
|
|
104
130
|
const formData = new FormData();
|
|
105
131
|
formData.append("file", new Blob([buffer], { type: mimeType }), filename);
|
|
106
|
-
formData.append("purpose", "image");
|
|
132
|
+
formData.append("purpose", isVideo ? "video" : "image");
|
|
107
133
|
|
|
108
134
|
const uploadUrl = `${deriveFilesBaseUrl(getBaseUrl())}/files`;
|
|
109
135
|
const debug = process.env.KIMI_CODE_DEBUG === "1";
|
|
@@ -113,13 +139,31 @@ export async function uploadKimiFile(
|
|
|
113
139
|
);
|
|
114
140
|
}
|
|
115
141
|
|
|
116
|
-
|
|
117
|
-
|
|
142
|
+
const postUpload = (token: string) =>
|
|
143
|
+
fetchImpl(uploadUrl, {
|
|
118
144
|
method: "POST",
|
|
119
|
-
headers: { ...getKimiProviderHeaders(), Authorization: `Bearer ${
|
|
145
|
+
headers: { ...getKimiProviderHeaders(), Authorization: `Bearer ${token}` },
|
|
120
146
|
body: formData,
|
|
121
147
|
});
|
|
122
|
-
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
let response = await postUpload(apiKey);
|
|
151
|
+
// Kimi access tokens are short-lived and invalidated server-side as soon
|
|
152
|
+
// as any peer process rotates them, so a 401 here usually means our key
|
|
153
|
+
// snapshot went stale mid-session, not that login is broken. Mirror the
|
|
154
|
+
// chat-stream recovery: force one refresh and retry once.
|
|
155
|
+
let responseText: string | undefined;
|
|
156
|
+
if (response.status === 401) {
|
|
157
|
+
responseText = await response.text();
|
|
158
|
+
const refreshed = await refreshAccessToken(apiKey);
|
|
159
|
+
if (refreshed && refreshed !== apiKey) {
|
|
160
|
+
console.error("[kimi-coding] upload got 401, retrying with refreshed token");
|
|
161
|
+
response = await postUpload(refreshed);
|
|
162
|
+
responseText = undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!response.ok)
|
|
166
|
+
throw new Error(`${response.status} ${responseText ?? (await response.text())}`);
|
|
123
167
|
const fileObj = (await response.json()) as { id?: string };
|
|
124
168
|
if (!fileObj.id) throw new Error("missing file id");
|
|
125
169
|
const fileUrl = `ms://${fileObj.id}`;
|
|
@@ -132,24 +176,69 @@ export async function uploadKimiFile(
|
|
|
132
176
|
}
|
|
133
177
|
|
|
134
178
|
// =============================================================================
|
|
135
|
-
// Payload file transformers
|
|
179
|
+
// Payload file transformers
|
|
136
180
|
//
|
|
137
181
|
// These walk the provider-specific payload shape and replace inline base64
|
|
138
182
|
// image blocks with ms:// references returned by the injected uploader. They
|
|
139
183
|
// take an Uploader rather than an apiKey so they can be unit-tested with a
|
|
140
184
|
// fake uploader; all network I/O stays behind that boundary.
|
|
185
|
+
//
|
|
186
|
+
// Successful uploads are remembered in a module-level cache shared across
|
|
187
|
+
// requests: payloads are rebuilt from session context every request, so
|
|
188
|
+
// without it a conversation's images would re-upload on every turn. Keys are
|
|
189
|
+
// content hashes rather than the data URLs themselves so the cache does not
|
|
190
|
+
// retain every image's base64 payload in memory. Failures are not cached and
|
|
191
|
+
// retry on the next request.
|
|
141
192
|
// =============================================================================
|
|
142
193
|
|
|
143
|
-
|
|
194
|
+
const MAX_UPLOADED_FILE_CACHE_ENTRIES = 512;
|
|
195
|
+
const uploadedFileCache = new Map<string, string>();
|
|
196
|
+
|
|
197
|
+
function uploadedFileCacheKey(cacheScope: string, mimeType: string, data: string): string {
|
|
198
|
+
return createHash("sha256")
|
|
199
|
+
.update(cacheScope)
|
|
200
|
+
.update("\0")
|
|
201
|
+
.update(mimeType)
|
|
202
|
+
.update("\0")
|
|
203
|
+
.update(data)
|
|
204
|
+
.digest("hex");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function rememberUploadedFile(
|
|
208
|
+
uploadCache: Map<string, string>,
|
|
209
|
+
cacheKey: string,
|
|
210
|
+
url: string,
|
|
211
|
+
): void {
|
|
212
|
+
if (
|
|
213
|
+
uploadCache === uploadedFileCache &&
|
|
214
|
+
!uploadCache.has(cacheKey) &&
|
|
215
|
+
uploadCache.size >= MAX_UPLOADED_FILE_CACHE_ENTRIES
|
|
216
|
+
) {
|
|
217
|
+
const oldest = uploadCache.keys().next().value;
|
|
218
|
+
if (oldest !== undefined) uploadCache.delete(oldest);
|
|
219
|
+
}
|
|
220
|
+
uploadCache.set(cacheKey, url);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function clearKimiUploadedFileCache(): void {
|
|
224
|
+
uploadedFileCache.clear();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function transformOpenAIPayloadFiles(
|
|
228
|
+
payload: JsonRecord,
|
|
229
|
+
upload: Uploader,
|
|
230
|
+
uploadCache: Map<string, string>,
|
|
231
|
+
uploadCacheScope: string,
|
|
232
|
+
): Promise<void> {
|
|
144
233
|
if (!Array.isArray(payload.messages)) return;
|
|
145
|
-
const cache = new Map<string, string>();
|
|
146
234
|
|
|
147
235
|
for (const message of payload.messages) {
|
|
148
236
|
if (!isRecord(message) || !Array.isArray(message.content)) continue;
|
|
149
237
|
|
|
150
238
|
for (const block of message.content) {
|
|
151
239
|
if (!isRecord(block)) continue;
|
|
152
|
-
const key =
|
|
240
|
+
const key =
|
|
241
|
+
block.type === "image_url" ? "image_url" : block.type === "video_url" ? "video_url" : null;
|
|
153
242
|
if (!key) continue;
|
|
154
243
|
|
|
155
244
|
const field = block[key];
|
|
@@ -164,9 +253,10 @@ async function transformOpenAIPayloadFiles(payload: JsonRecord, upload: Uploader
|
|
|
164
253
|
const parsed = parseDataUrl(urlValue);
|
|
165
254
|
if (!parsed) continue;
|
|
166
255
|
|
|
167
|
-
const
|
|
256
|
+
const cacheKey = uploadedFileCacheKey(uploadCacheScope, parsed.mimeType, parsed.data);
|
|
257
|
+
const uploaded = uploadCache.get(cacheKey) ?? (await upload(parsed.mimeType, parsed.data));
|
|
168
258
|
if (!uploaded) continue;
|
|
169
|
-
|
|
259
|
+
rememberUploadedFile(uploadCache, cacheKey, uploaded);
|
|
170
260
|
|
|
171
261
|
block[key] =
|
|
172
262
|
typeof field === "string" ? uploaded : { ...(field as JsonRecord), url: uploaded };
|
|
@@ -330,9 +420,10 @@ function normalizeOpenAIToolSchemas(payload: JsonRecord): void {
|
|
|
330
420
|
async function transformAnthropicPayloadFiles(
|
|
331
421
|
payload: JsonRecord,
|
|
332
422
|
upload: Uploader,
|
|
423
|
+
uploadCache: Map<string, string>,
|
|
424
|
+
uploadCacheScope: string,
|
|
333
425
|
): Promise<void> {
|
|
334
426
|
if (!Array.isArray(payload.messages)) return;
|
|
335
|
-
const cache = new Map<string, string>();
|
|
336
427
|
|
|
337
428
|
const transformImageBlock = async (block: unknown): Promise<unknown> => {
|
|
338
429
|
if (!isRecord(block) || block.type !== "image") return block;
|
|
@@ -342,10 +433,10 @@ async function transformAnthropicPayloadFiles(
|
|
|
342
433
|
const data = source.data;
|
|
343
434
|
if (typeof mediaType !== "string" || typeof data !== "string") return block;
|
|
344
435
|
|
|
345
|
-
const cacheKey =
|
|
346
|
-
const uploaded =
|
|
436
|
+
const cacheKey = uploadedFileCacheKey(uploadCacheScope, mediaType, data);
|
|
437
|
+
const uploaded = uploadCache.get(cacheKey) ?? (await upload(mediaType, data));
|
|
347
438
|
if (!uploaded) return block;
|
|
348
|
-
|
|
439
|
+
rememberUploadedFile(uploadCache, cacheKey, uploaded);
|
|
349
440
|
|
|
350
441
|
const next: JsonRecord = { type: "image", source: { type: "url", url: uploaded } };
|
|
351
442
|
if (block.cache_control !== undefined) next.cache_control = block.cache_control;
|
|
@@ -390,10 +481,12 @@ export async function applyKimiPayloadMutations(
|
|
|
390
481
|
|
|
391
482
|
// 2. File upload dispatch (protocol-specific).
|
|
392
483
|
if (ctx.upload) {
|
|
484
|
+
const uploadCache = ctx.uploadCacheScope ? uploadedFileCache : new Map<string, string>();
|
|
485
|
+
const uploadCacheScope = ctx.uploadCacheScope ?? "request";
|
|
393
486
|
if (ctx.api === "openai-completions") {
|
|
394
|
-
await transformOpenAIPayloadFiles(payload, ctx.upload);
|
|
487
|
+
await transformOpenAIPayloadFiles(payload, ctx.upload, uploadCache, uploadCacheScope);
|
|
395
488
|
} else if (ctx.api === "anthropic-messages") {
|
|
396
|
-
await transformAnthropicPayloadFiles(payload, ctx.upload);
|
|
489
|
+
await transformAnthropicPayloadFiles(payload, ctx.upload, uploadCache, uploadCacheScope);
|
|
397
490
|
}
|
|
398
491
|
}
|
|
399
492
|
if (ctx.api === "openai-completions") {
|
|
@@ -447,8 +540,17 @@ export async function applyKimiPayloadMutations(
|
|
|
447
540
|
}
|
|
448
541
|
|
|
449
542
|
const generation = ctx.modelConfig.generation;
|
|
543
|
+
// Official kimi-code sends temperature/top_p only when explicitly configured
|
|
544
|
+
// (env KIMI_MODEL_TEMPERATURE / KIMI_MODEL_TOP_P → generation.*); otherwise it
|
|
545
|
+
// omits them and lets the server apply its own defaults. Mirror that exactly:
|
|
546
|
+
// drop any value pi-ai seeded by default when the user hasn't configured one
|
|
547
|
+
// (the official client does not pin these to 1.0 / 0.95).
|
|
450
548
|
if (generation.temperature !== undefined) payload.temperature = generation.temperature;
|
|
549
|
+
else delete payload.temperature;
|
|
451
550
|
if (generation.topP !== undefined) payload.top_p = generation.topP;
|
|
551
|
+
else delete payload.top_p;
|
|
552
|
+
// Output cap: an explicit cap (KIMI_MODEL_MAX_COMPLETION_TOKENS / config →
|
|
553
|
+
// generation.maxCompletionTokens) is honored as a hardCap on the request value.
|
|
452
554
|
if (generation.maxCompletionTokens !== undefined) {
|
|
453
555
|
const maxTokensKey = ctx.api === "anthropic-messages" ? "max_tokens" : "max_completion_tokens";
|
|
454
556
|
const currentMaxTokens = payload[maxTokensKey];
|
|
@@ -469,22 +571,39 @@ export async function applyKimiPayloadMutations(
|
|
|
469
571
|
const mapped = resolveReasoningForLevel(resolvedReasoning, ctx.modelConfig);
|
|
470
572
|
if (mapped) {
|
|
471
573
|
const oldThinking = isRecord(payload.thinking) ? payload.thinking : {};
|
|
472
|
-
const thinking: JsonRecord = {
|
|
473
|
-
...oldThinking,
|
|
474
|
-
type: mapped.enabled ? "enabled" : "disabled",
|
|
475
|
-
};
|
|
476
|
-
delete thinking.effort;
|
|
477
|
-
if (!mapped.enabled) delete thinking.keep;
|
|
478
574
|
const effort = ctx.reasoning
|
|
479
575
|
? mapped.effort
|
|
480
576
|
: (ctx.modelConfig.defaultEffort ?? mapped.effort);
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
577
|
+
const effortSupported =
|
|
578
|
+
effort !== null && ctx.modelConfig.supportEfforts?.includes(effort) === true;
|
|
579
|
+
if (ctx.api === "anthropic-messages" && oldThinking.type === "adaptive") {
|
|
580
|
+
// pi-ai >=0.82 builds adaptive thinking for models carrying
|
|
581
|
+
// compat.forceAdaptiveThinking (streamSimpleKimi sets it on the
|
|
582
|
+
// anthropic runtime model). Keep the adaptive shape — effort lives in
|
|
583
|
+
// top-level output_config there, not inside thinking — and only
|
|
584
|
+
// replace it with an explicit disable when the level maps to off.
|
|
585
|
+
if (mapped.enabled) {
|
|
586
|
+
if (effortSupported) payload.output_config = { effort };
|
|
587
|
+
else delete payload.output_config;
|
|
588
|
+
} else {
|
|
589
|
+
payload.thinking = { type: "disabled" };
|
|
590
|
+
delete payload.output_config;
|
|
591
|
+
}
|
|
592
|
+
} else {
|
|
593
|
+
const thinking: JsonRecord = {
|
|
594
|
+
...oldThinking,
|
|
595
|
+
type: mapped.enabled ? "enabled" : "disabled",
|
|
596
|
+
};
|
|
597
|
+
delete thinking.effort;
|
|
598
|
+
if (!mapped.enabled) delete thinking.keep;
|
|
599
|
+
if (mapped.enabled && effortSupported) {
|
|
600
|
+
thinking.effort = effort;
|
|
601
|
+
}
|
|
602
|
+
if (mapped.enabled && ctx.modelConfig.thinkingKeep) {
|
|
603
|
+
thinking.keep = ctx.modelConfig.thinkingKeep;
|
|
604
|
+
}
|
|
605
|
+
payload.thinking = thinking;
|
|
486
606
|
}
|
|
487
|
-
payload.thinking = thinking;
|
|
488
607
|
}
|
|
489
608
|
}
|
|
490
609
|
|
|
@@ -510,15 +629,10 @@ export async function applyKimiPayloadMutations(
|
|
|
510
629
|
}
|
|
511
630
|
}
|
|
512
631
|
|
|
513
|
-
// 8. K2.7 Code API constraints: the server rejects
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
|
|
517
|
-
delete payload.temperature;
|
|
518
|
-
}
|
|
519
|
-
if (payload.top_p !== undefined && payload.top_p !== 0.95) {
|
|
520
|
-
delete payload.top_p;
|
|
521
|
-
}
|
|
632
|
+
// 8. K2.7 Code API constraints: the server rejects tool_choice "required" /
|
|
633
|
+
// function-specific when thinking is enabled (always-on). temperature/top_p
|
|
634
|
+
// are handled in step 6 — omitted unless explicitly configured, matching the
|
|
635
|
+
// official kimi-code client rather than pinned to 1.0/0.95.
|
|
522
636
|
if (payload.tool_choice !== undefined) {
|
|
523
637
|
const tc = payload.tool_choice;
|
|
524
638
|
const isAllowed =
|
package/src/stream.ts
CHANGED
|
@@ -58,9 +58,13 @@ import {
|
|
|
58
58
|
type KimiResolvedModelConfig,
|
|
59
59
|
} from "./config.ts";
|
|
60
60
|
|
|
61
|
-
import { ENV_KIMI_CODE_PROTOCOL, getApiProtocol, getBaseUrl } from "./constants.ts";
|
|
61
|
+
import { ENV_KIMI_CODE_PROTOCOL, PROVIDER_ID, getApiProtocol, getBaseUrl } from "./constants.ts";
|
|
62
62
|
import { getKimiProviderHeaders } from "./device.ts";
|
|
63
|
-
import {
|
|
63
|
+
import {
|
|
64
|
+
isKimiAuthErrorMessage,
|
|
65
|
+
readStoredOAuthCredential,
|
|
66
|
+
refreshKimiAuthToken,
|
|
67
|
+
} from "./oauth.ts";
|
|
64
68
|
import {
|
|
65
69
|
type Uploader,
|
|
66
70
|
applyKimiPayloadMutations,
|
|
@@ -190,11 +194,21 @@ export async function* filterEmptyResponseStream(
|
|
|
190
194
|
|
|
191
195
|
const KIMI_API_KEY_ENV_REFERENCES = new Set(["$KIMI_API_KEY", "${KIMI_API_KEY}"]);
|
|
192
196
|
|
|
197
|
+
// On the normal request path pi resolves the provider credential through its
|
|
198
|
+
// ModelRuntime and hands it to us. Callers that arrive through pi-ai's global
|
|
199
|
+
// api registry (a bare agentLoop() with no streamFn) have no ModelRuntime
|
|
200
|
+
// behind them and pass nothing, so read the same credential store pi itself
|
|
201
|
+
// would have read instead of sending an unauthenticated request. Kept last so
|
|
202
|
+
// an explicit key and the environment override keep their existing precedence.
|
|
203
|
+
function getStoredKimiAccessToken(): string {
|
|
204
|
+
return readStoredOAuthCredential(PROVIDER_ID)?.access?.trim() || "";
|
|
205
|
+
}
|
|
206
|
+
|
|
193
207
|
export function resolveKimiApiKey(apiKey: string | undefined): string {
|
|
194
208
|
if (apiKey !== undefined && KIMI_API_KEY_ENV_REFERENCES.has(apiKey)) {
|
|
195
|
-
return process.env.KIMI_API_KEY?.trim() ||
|
|
209
|
+
return process.env.KIMI_API_KEY?.trim() || getStoredKimiAccessToken();
|
|
196
210
|
}
|
|
197
|
-
return apiKey || process.env.KIMI_API_KEY ||
|
|
211
|
+
return apiKey || process.env.KIMI_API_KEY || getStoredKimiAccessToken();
|
|
198
212
|
}
|
|
199
213
|
|
|
200
214
|
// SimpleStreamOptions.headers is Record<string, string> on pi <=0.79 and
|
|
@@ -285,6 +299,7 @@ export function streamSimpleKimi(
|
|
|
285
299
|
await applyKimiPayloadMutations(nextPayload, {
|
|
286
300
|
api: apiProtocol,
|
|
287
301
|
upload,
|
|
302
|
+
uploadCacheScope: apiKey ? `${getBaseUrl(wireProtocol)}:${apiKey}` : undefined,
|
|
288
303
|
cacheKey,
|
|
289
304
|
cacheRetention,
|
|
290
305
|
reasoning: options?.reasoning,
|
|
@@ -323,6 +338,19 @@ export function streamSimpleKimi(
|
|
|
323
338
|
...model,
|
|
324
339
|
api: apiProtocol,
|
|
325
340
|
baseUrl: getBaseUrl(wireProtocol),
|
|
341
|
+
// Kimi's anthropic-protocol endpoint speaks adaptive thinking and
|
|
342
|
+
// rejects/ignores the interleaved-thinking beta header; pi-ai >=0.82
|
|
343
|
+
// keys both behaviors off these compat flags (its own built-in
|
|
344
|
+
// kimi-coding models carry them in generated metadata).
|
|
345
|
+
...(wireProtocol === "anthropic"
|
|
346
|
+
? {
|
|
347
|
+
compat: {
|
|
348
|
+
...(model as { compat?: Record<string, unknown> }).compat,
|
|
349
|
+
forceAdaptiveThinking: true,
|
|
350
|
+
allowEmptySignature: true,
|
|
351
|
+
},
|
|
352
|
+
}
|
|
353
|
+
: {}),
|
|
326
354
|
} as Model<Api>;
|
|
327
355
|
const upstream =
|
|
328
356
|
wireProtocol === "openai"
|
package/src/usage.ts
CHANGED
|
@@ -2,15 +2,6 @@ import { PROVIDER_ID, getBaseUrl } from "./constants.ts";
|
|
|
2
2
|
import { getKimiProviderHeaders } from "./device.ts";
|
|
3
3
|
import { readStoredOAuthCredential, refreshKimiAuthToken } from "./oauth.ts";
|
|
4
4
|
|
|
5
|
-
const MEMBERSHIP_LEVEL_NAMES: Record<string, string> = {
|
|
6
|
-
LEVEL_FREE: "Free",
|
|
7
|
-
LEVEL_BASIC: "Adagio",
|
|
8
|
-
LEVEL_STANDARD: "Moderato",
|
|
9
|
-
LEVEL_INTERMEDIATE: "Allegretto",
|
|
10
|
-
LEVEL_ADVANCED: "Allegro",
|
|
11
|
-
LEVEL_PREMIUM: "Vivace",
|
|
12
|
-
};
|
|
13
|
-
|
|
14
5
|
const RESET_TIME_KEYS = [
|
|
15
6
|
"resetTime",
|
|
16
7
|
"reset_time",
|
|
@@ -36,7 +27,6 @@ export interface UsageFormatOptions {
|
|
|
36
27
|
|
|
37
28
|
export interface KimiUsageSnapshot {
|
|
38
29
|
summary: string;
|
|
39
|
-
membershipLevel: string | null;
|
|
40
30
|
}
|
|
41
31
|
|
|
42
32
|
interface BoosterWalletInfo {
|
|
@@ -174,9 +164,6 @@ export function parseUsageSummary(payload: unknown, options: UsageFormatOptions
|
|
|
174
164
|
|
|
175
165
|
const record = payload as Record<string, unknown>;
|
|
176
166
|
const lines: string[] = [];
|
|
177
|
-
const membership = parseMembership(record);
|
|
178
|
-
if (membership) lines.push(membership, "");
|
|
179
|
-
|
|
180
167
|
const summary = parseUsageRow(record.usage, "Current week");
|
|
181
168
|
if (summary)
|
|
182
169
|
lines.push(formatUsageRow({ ...summary, label: normalizeUsageLabel(summary.label) }, options));
|
|
@@ -235,24 +222,6 @@ function formatWindowLabel(value: unknown, fallbackLabel: string): string {
|
|
|
235
222
|
return `Current ${minutes}m window`;
|
|
236
223
|
}
|
|
237
224
|
|
|
238
|
-
export function parseMembershipLevel(record: Record<string, unknown>): string | null {
|
|
239
|
-
const user = record.user;
|
|
240
|
-
if (typeof user !== "object" || user === null || Array.isArray(user)) return null;
|
|
241
|
-
const membership = (user as Record<string, unknown>).membership;
|
|
242
|
-
if (typeof membership !== "object" || membership === null || Array.isArray(membership)) {
|
|
243
|
-
return null;
|
|
244
|
-
}
|
|
245
|
-
const level = (membership as Record<string, unknown>).level;
|
|
246
|
-
return typeof level === "string" && level ? level : null;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export function parseMembership(record: Record<string, unknown>): string | null {
|
|
250
|
-
const level = parseMembershipLevel(record);
|
|
251
|
-
if (!level) return null;
|
|
252
|
-
const name = MEMBERSHIP_LEVEL_NAMES[level];
|
|
253
|
-
return name ? `Membership: ${name} (${level})` : `Membership: ${level}`;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
225
|
export function formatUsageRow(row: UsageRow, options: UsageFormatOptions = {}): string {
|
|
257
226
|
if (row.limit <= 0) return `${row.label}\n${row.used} used`;
|
|
258
227
|
const used = Math.max(0, Math.min(row.used, row.limit));
|
|
@@ -356,10 +325,7 @@ export async function fetchKimiUsageSnapshot(
|
|
|
356
325
|
): Promise<KimiUsageSnapshot> {
|
|
357
326
|
const token = options.token ?? getKimiUsageToken();
|
|
358
327
|
if (!token) {
|
|
359
|
-
return {
|
|
360
|
-
summary: "Usage: missing credentials. Run /login kimi-coding.",
|
|
361
|
-
membershipLevel: null,
|
|
362
|
-
};
|
|
328
|
+
return { summary: "Usage: missing credentials. Run /login kimi-coding." };
|
|
363
329
|
}
|
|
364
330
|
|
|
365
331
|
const controller = new AbortController();
|
|
@@ -371,17 +337,13 @@ export async function fetchKimiUsageSnapshot(
|
|
|
371
337
|
if (refreshed) response = await fetchKimiUsage(refreshed, controller.signal);
|
|
372
338
|
}
|
|
373
339
|
if (!response.ok) {
|
|
374
|
-
return { summary: `Usage: fetch failed (${response.status})
|
|
340
|
+
return { summary: `Usage: fetch failed (${response.status})` };
|
|
375
341
|
}
|
|
376
342
|
const payload = (await response.json()) as unknown;
|
|
377
|
-
|
|
378
|
-
typeof payload === "object" && payload !== null && !Array.isArray(payload)
|
|
379
|
-
? parseMembershipLevel(payload as Record<string, unknown>)
|
|
380
|
-
: null;
|
|
381
|
-
return { summary: parseUsageSummary(payload), membershipLevel };
|
|
343
|
+
return { summary: parseUsageSummary(payload) };
|
|
382
344
|
} catch (error) {
|
|
383
345
|
const message = error instanceof Error ? error.message : String(error);
|
|
384
|
-
return { summary: `Usage: fetch failed (${message})
|
|
346
|
+
return { summary: `Usage: fetch failed (${message})` };
|
|
385
347
|
} finally {
|
|
386
348
|
clearTimeout(timeout);
|
|
387
349
|
}
|