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/payload.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { CacheRetention, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
|
6
6
|
import type { KimiResolvedModelConfig, ModelReasoningEntry } from "./config.ts";
|
|
7
7
|
|
|
8
8
|
import { getBaseUrl } from "./constants.ts";
|
|
9
|
-
import {
|
|
9
|
+
import { getKimiProviderHeaders } from "./device.ts";
|
|
10
10
|
import { optimizeToolSchemas } from "./schema-dedup.ts";
|
|
11
11
|
|
|
12
12
|
// =============================================================================
|
|
@@ -116,7 +116,7 @@ export async function uploadKimiFile(
|
|
|
116
116
|
try {
|
|
117
117
|
const response = await fetch(uploadUrl, {
|
|
118
118
|
method: "POST",
|
|
119
|
-
headers: { Authorization: `Bearer ${apiKey}
|
|
119
|
+
headers: { ...getKimiProviderHeaders(), Authorization: `Bearer ${apiKey}` },
|
|
120
120
|
body: formData,
|
|
121
121
|
});
|
|
122
122
|
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
|
|
@@ -425,7 +425,19 @@ export async function applyKimiPayloadMutations(
|
|
|
425
425
|
: { include_usage: true };
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
-
// 5.
|
|
428
|
+
// 5. Spread extra_body into the top-level payload before normalization and
|
|
429
|
+
// config caps. Top-level fields retain precedence over extra_body.
|
|
430
|
+
if (isRecord(payload.extra_body)) {
|
|
431
|
+
const extraBody = payload.extra_body as JsonRecord;
|
|
432
|
+
delete payload.extra_body;
|
|
433
|
+
for (const [key, value] of Object.entries(extraBody)) {
|
|
434
|
+
if (payload[key] === undefined) {
|
|
435
|
+
payload[key] = value;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// 6. Normalize deprecated max_tokens (OpenAI path only — Anthropic
|
|
429
441
|
// /messages uses max_tokens natively).
|
|
430
442
|
if (ctx.api === "openai-completions") {
|
|
431
443
|
if (payload.max_completion_tokens === undefined && typeof payload.max_tokens === "number") {
|
|
@@ -438,44 +450,67 @@ export async function applyKimiPayloadMutations(
|
|
|
438
450
|
if (generation.temperature !== undefined) payload.temperature = generation.temperature;
|
|
439
451
|
if (generation.topP !== undefined) payload.top_p = generation.topP;
|
|
440
452
|
if (generation.maxCompletionTokens !== undefined) {
|
|
441
|
-
|
|
453
|
+
const maxTokensKey = ctx.api === "anthropic-messages" ? "max_tokens" : "max_completion_tokens";
|
|
454
|
+
const currentMaxTokens = payload[maxTokensKey];
|
|
455
|
+
payload[maxTokensKey] =
|
|
456
|
+
typeof currentMaxTokens === "number"
|
|
457
|
+
? Math.min(currentMaxTokens, generation.maxCompletionTokens)
|
|
458
|
+
: generation.maxCompletionTokens;
|
|
442
459
|
}
|
|
443
460
|
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
delete payload.
|
|
449
|
-
for (const [key, value] of Object.entries(extraBody)) {
|
|
450
|
-
if (payload[key] === undefined) {
|
|
451
|
-
payload[key] = value;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
461
|
+
// 7. Reasoning effort mapping. Kimi now accepts effort only inside the
|
|
462
|
+
// thinking object, and only for values advertised by the model catalog.
|
|
463
|
+
delete payload.reasoning_effort;
|
|
464
|
+
if (ctx.modelConfig.supportsThinkingType === "no") {
|
|
465
|
+
delete payload.thinking;
|
|
454
466
|
}
|
|
455
|
-
|
|
456
|
-
// 6. Reasoning effort mapping. Merges into any existing top-level thinking
|
|
457
|
-
// field (which may have come from extra_body above).
|
|
458
467
|
const resolvedReasoning = resolveThinkingLevel(ctx);
|
|
459
468
|
if (resolvedReasoning) {
|
|
460
469
|
const mapped = resolveReasoningForLevel(resolvedReasoning, ctx.modelConfig);
|
|
461
470
|
if (mapped) {
|
|
462
|
-
if (mapped.effort !== null) {
|
|
463
|
-
payload.reasoning_effort = mapped.effort;
|
|
464
|
-
} else {
|
|
465
|
-
delete payload.reasoning_effort;
|
|
466
|
-
}
|
|
467
471
|
const oldThinking = isRecord(payload.thinking) ? payload.thinking : {};
|
|
468
|
-
|
|
472
|
+
const thinking: JsonRecord = {
|
|
469
473
|
...oldThinking,
|
|
470
474
|
type: mapped.enabled ? "enabled" : "disabled",
|
|
471
475
|
};
|
|
476
|
+
delete thinking.effort;
|
|
477
|
+
if (!mapped.enabled) delete thinking.keep;
|
|
478
|
+
const effort = ctx.reasoning
|
|
479
|
+
? mapped.effort
|
|
480
|
+
: (ctx.modelConfig.defaultEffort ?? mapped.effort);
|
|
481
|
+
if (mapped.enabled && effort !== null && ctx.modelConfig.supportEfforts?.includes(effort)) {
|
|
482
|
+
thinking.effort = effort;
|
|
483
|
+
}
|
|
472
484
|
if (mapped.enabled && ctx.modelConfig.thinkingKeep) {
|
|
473
|
-
|
|
485
|
+
thinking.keep = ctx.modelConfig.thinkingKeep;
|
|
486
|
+
}
|
|
487
|
+
payload.thinking = thinking;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Preserved-thinking Kimi endpoints require every replayed assistant turn to
|
|
492
|
+
// carry reasoning_content, including turns whose reasoning delta was empty.
|
|
493
|
+
// pi-ai drops empty thinking blocks while building Chat Completions history,
|
|
494
|
+
// so restore the explicit empty field after the final thinking mode is known.
|
|
495
|
+
if (
|
|
496
|
+
ctx.api === "openai-completions" &&
|
|
497
|
+
isRecord(payload.thinking) &&
|
|
498
|
+
payload.thinking.type !== "disabled" &&
|
|
499
|
+
payload.thinking.keep === "all" &&
|
|
500
|
+
Array.isArray(payload.messages)
|
|
501
|
+
) {
|
|
502
|
+
for (const message of payload.messages) {
|
|
503
|
+
if (
|
|
504
|
+
isRecord(message) &&
|
|
505
|
+
message.role === "assistant" &&
|
|
506
|
+
message.reasoning_content === undefined
|
|
507
|
+
) {
|
|
508
|
+
message.reasoning_content = "";
|
|
474
509
|
}
|
|
475
510
|
}
|
|
476
511
|
}
|
|
477
512
|
|
|
478
|
-
//
|
|
513
|
+
// 8. K2.7 Code API constraints: the server rejects non-default values for
|
|
479
514
|
// temperature (must be 1.0) and top_p (must be 0.95), and tool_choice
|
|
480
515
|
// "required" / function-specific when thinking is enabled (always-on).
|
|
481
516
|
if (payload.temperature !== undefined && payload.temperature !== 1) {
|
package/src/stream.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "./config.ts";
|
|
25
25
|
|
|
26
26
|
import { ENV_KIMI_CODE_PROTOCOL, getApiProtocol, getBaseUrl } from "./constants.ts";
|
|
27
|
-
import {
|
|
27
|
+
import { getKimiProviderHeaders } from "./device.ts";
|
|
28
28
|
import { isKimiAuthErrorMessage, refreshKimiAuthToken } from "./oauth.ts";
|
|
29
29
|
import {
|
|
30
30
|
type Uploader,
|
|
@@ -163,7 +163,7 @@ export function resolveKimiApiKey(apiKey: string | undefined): string {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
export function mergeKimiRequestHeaders(headers?: Record<string, string>): Record<string, string> {
|
|
166
|
-
return { ...
|
|
166
|
+
return { ...getKimiProviderHeaders(), ...headers };
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
export function streamSimpleKimi(
|
|
@@ -192,8 +192,27 @@ export function streamSimpleKimi(
|
|
|
192
192
|
protocol: ENV_KIMI_CODE_PROTOCOL,
|
|
193
193
|
uploads: DEFAULT_KIMI_CODE_CONFIG.uploads,
|
|
194
194
|
};
|
|
195
|
-
const
|
|
196
|
-
|
|
195
|
+
const discoveredModel = model as Model<Api> & {
|
|
196
|
+
supportsThinkingType?: "only" | "no" | "both";
|
|
197
|
+
wireProtocol?: KimiCodeConfig["protocol"];
|
|
198
|
+
supportEfforts?: string[];
|
|
199
|
+
defaultEffort?: string;
|
|
200
|
+
};
|
|
201
|
+
const modelConfig: KimiResolvedModelConfig = {
|
|
202
|
+
...streamConfig.model,
|
|
203
|
+
contextWindow: model.contextWindow,
|
|
204
|
+
maxTokens: model.maxTokens,
|
|
205
|
+
input: [...model.input],
|
|
206
|
+
reasoning: model.reasoning,
|
|
207
|
+
supportsThinkingType:
|
|
208
|
+
discoveredModel.supportsThinkingType ?? (model.reasoning ? undefined : "no"),
|
|
209
|
+
supportEfforts: discoveredModel.supportEfforts
|
|
210
|
+
? [...discoveredModel.supportEfforts]
|
|
211
|
+
: undefined,
|
|
212
|
+
defaultEffort: discoveredModel.defaultEffort,
|
|
213
|
+
};
|
|
214
|
+
const wireProtocol = discoveredModel.wireProtocol ?? streamConfig.protocol;
|
|
215
|
+
const apiProtocol = getApiProtocol(wireProtocol);
|
|
197
216
|
const originalOnPayload = options?.onPayload;
|
|
198
217
|
// The pi-side model id ("kimi-for-coding") is what users select via /model
|
|
199
218
|
// and what gets persisted into sessions. The wire model id discovered at
|
|
@@ -257,10 +276,10 @@ export function streamSimpleKimi(
|
|
|
257
276
|
const runtimeModel = {
|
|
258
277
|
...model,
|
|
259
278
|
api: apiProtocol,
|
|
260
|
-
baseUrl: getBaseUrl(
|
|
279
|
+
baseUrl: getBaseUrl(wireProtocol),
|
|
261
280
|
} as Model<Api>;
|
|
262
281
|
const upstream =
|
|
263
|
-
|
|
282
|
+
wireProtocol === "openai"
|
|
264
283
|
? streamSimpleOpenAICompletions(
|
|
265
284
|
runtimeModel as Model<"openai-completions">,
|
|
266
285
|
context,
|
package/src/tools/common.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { AuthStorage, type AgentToolResult } from "@earendil-works/pi-coding-age
|
|
|
2
2
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
|
|
4
4
|
import { PROVIDER_ID, getBaseUrl } from "../constants.ts";
|
|
5
|
-
import {
|
|
6
|
-
import { refreshKimiAuthToken } from "../oauth.ts";
|
|
5
|
+
import { getKimiProviderHeaders } from "../device.ts";
|
|
6
|
+
import { KIMI_LOGIN_REQUIRED_MESSAGE, refreshKimiAuthToken } from "../oauth.ts";
|
|
7
7
|
|
|
8
8
|
export const KIMI_TOOL_TIMEOUT_MS = 180_000;
|
|
9
9
|
|
|
@@ -67,7 +67,7 @@ export function getKimiDatasourceUrl(): string {
|
|
|
67
67
|
|
|
68
68
|
export function buildHeaders(accessToken: string, toolCallId: string): Record<string, string> {
|
|
69
69
|
return {
|
|
70
|
-
...
|
|
70
|
+
...getKimiProviderHeaders(),
|
|
71
71
|
Authorization: `Bearer ${accessToken}`,
|
|
72
72
|
"Content-Type": "application/json",
|
|
73
73
|
"X-Msh-Tool-Call-Id": toolCallId,
|
|
@@ -94,7 +94,9 @@ export async function fetchWithAuthRetry(
|
|
|
94
94
|
if (response.status !== 401) return response;
|
|
95
95
|
|
|
96
96
|
const refreshed = await deps.refreshAccessToken(accessToken);
|
|
97
|
-
if (!refreshed || refreshed === accessToken)
|
|
97
|
+
if (!refreshed || refreshed === accessToken) {
|
|
98
|
+
throw new Error(KIMI_LOGIN_REQUIRED_MESSAGE);
|
|
99
|
+
}
|
|
98
100
|
return request(refreshed);
|
|
99
101
|
}
|
|
100
102
|
|
package/src/tools/moonshot.ts
CHANGED
|
@@ -17,20 +17,6 @@ import {
|
|
|
17
17
|
|
|
18
18
|
export const moonshotSearchSchema = Type.Object({
|
|
19
19
|
query: Type.String({ description: "The query text to search for." }),
|
|
20
|
-
limit: Type.Optional(
|
|
21
|
-
Type.Number({
|
|
22
|
-
description: "The number of results to return.",
|
|
23
|
-
default: 5,
|
|
24
|
-
minimum: 1,
|
|
25
|
-
maximum: 20,
|
|
26
|
-
}),
|
|
27
|
-
),
|
|
28
|
-
include_content: Type.Optional(
|
|
29
|
-
Type.Boolean({
|
|
30
|
-
description: "Whether to include fetched page content in the search results.",
|
|
31
|
-
default: false,
|
|
32
|
-
}),
|
|
33
|
-
),
|
|
34
20
|
});
|
|
35
21
|
|
|
36
22
|
export const moonshotFetchSchema = Type.Object({
|
|
@@ -44,7 +30,8 @@ export interface MoonshotSearchResult {
|
|
|
44
30
|
url: string;
|
|
45
31
|
title: string;
|
|
46
32
|
snippet: string;
|
|
47
|
-
|
|
33
|
+
date?: string;
|
|
34
|
+
siteName?: string;
|
|
48
35
|
}
|
|
49
36
|
|
|
50
37
|
export interface MoonshotFetchResult {
|
|
@@ -52,11 +39,6 @@ export interface MoonshotFetchResult {
|
|
|
52
39
|
content: string;
|
|
53
40
|
}
|
|
54
41
|
|
|
55
|
-
function clampLimit(value: number | undefined): number {
|
|
56
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return 5;
|
|
57
|
-
return Math.max(1, Math.min(20, Math.trunc(value)));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
42
|
interface MoonshotSearchResponse {
|
|
61
43
|
search_results?: unknown;
|
|
62
44
|
}
|
|
@@ -66,7 +48,7 @@ function stringField(record: Record<string, unknown>, key: string): string {
|
|
|
66
48
|
return typeof value === "string" ? value : "";
|
|
67
49
|
}
|
|
68
50
|
|
|
69
|
-
function mapSearchResults(value: unknown
|
|
51
|
+
function mapSearchResults(value: unknown): MoonshotSearchResult[] {
|
|
70
52
|
if (!Array.isArray(value)) return [];
|
|
71
53
|
return value
|
|
72
54
|
.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null)
|
|
@@ -76,9 +58,10 @@ function mapSearchResults(value: unknown, includeContent: boolean): MoonshotSear
|
|
|
76
58
|
title: stringField(item, "title"),
|
|
77
59
|
snippet: stringField(item, "snippet"),
|
|
78
60
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
61
|
+
const date = stringField(item, "date");
|
|
62
|
+
if (date) result.date = date;
|
|
63
|
+
const siteName = stringField(item, "site_name");
|
|
64
|
+
if (siteName) result.siteName = siteName;
|
|
82
65
|
return result;
|
|
83
66
|
});
|
|
84
67
|
}
|
|
@@ -130,8 +113,10 @@ export function buildMoonshotSearchTool(options: BuildKimiToolOptions = {}) {
|
|
|
130
113
|
return defineTool({
|
|
131
114
|
name: "moonshot_search",
|
|
132
115
|
label: "Moonshot Search",
|
|
133
|
-
description:
|
|
134
|
-
|
|
116
|
+
description:
|
|
117
|
+
"Search the web through Kimi Coding. Results are summaries; use moonshot_fetch to read relevant pages.",
|
|
118
|
+
promptSnippet:
|
|
119
|
+
"Search the web with Kimi Coding, then fetch the few relevant result URLs before answering",
|
|
135
120
|
parameters: moonshotSearchSchema,
|
|
136
121
|
|
|
137
122
|
async execute(toolCallId, params, signal) {
|
|
@@ -142,20 +127,13 @@ export function buildMoonshotSearchTool(options: BuildKimiToolOptions = {}) {
|
|
|
142
127
|
);
|
|
143
128
|
}
|
|
144
129
|
|
|
145
|
-
const limit = clampLimit(params.limit);
|
|
146
|
-
const includeContent = params.include_content === true;
|
|
147
130
|
const timeout = buildTimeoutSignal(signal);
|
|
148
131
|
try {
|
|
149
132
|
const response = await fetchWithAuthRetry(deps, accessToken, (token) =>
|
|
150
133
|
deps.fetch(`${getKimiBaseV1()}/search`, {
|
|
151
134
|
method: "POST",
|
|
152
135
|
headers: buildHeaders(token, toolCallId),
|
|
153
|
-
body: JSON.stringify({
|
|
154
|
-
text_query: params.query,
|
|
155
|
-
limit,
|
|
156
|
-
enable_page_crawling: includeContent,
|
|
157
|
-
timeout_seconds: 30,
|
|
158
|
-
}),
|
|
136
|
+
body: JSON.stringify({ text_query: params.query }),
|
|
159
137
|
signal: timeout.signal,
|
|
160
138
|
}),
|
|
161
139
|
);
|
|
@@ -168,7 +146,7 @@ export function buildMoonshotSearchTool(options: BuildKimiToolOptions = {}) {
|
|
|
168
146
|
}
|
|
169
147
|
|
|
170
148
|
const data = (await response.json()) as MoonshotSearchResponse;
|
|
171
|
-
const results = mapSearchResults(data.search_results
|
|
149
|
+
const results = mapSearchResults(data.search_results);
|
|
172
150
|
return {
|
|
173
151
|
content: [{ type: "text", text: searchResultsText(results) }],
|
|
174
152
|
details: results,
|
package/src/usage.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AuthStorage } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
import { PROVIDER_ID, getBaseUrl } from "./constants.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { getKimiProviderHeaders } from "./device.ts";
|
|
5
5
|
import { refreshKimiAuthToken } from "./oauth.ts";
|
|
6
6
|
|
|
7
7
|
const MEMBERSHIP_LEVEL_NAMES: Record<string, string> = {
|
|
@@ -36,6 +36,21 @@ export interface UsageFormatOptions {
|
|
|
36
36
|
timeZone?: string;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export interface KimiUsageSnapshot {
|
|
40
|
+
summary: string;
|
|
41
|
+
membershipLevel: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface BoosterWalletInfo {
|
|
45
|
+
balanceCents: bigint;
|
|
46
|
+
monthlyChargeLimitEnabled: boolean;
|
|
47
|
+
monthlyChargeLimitCents: bigint;
|
|
48
|
+
monthlyUsedCents: bigint;
|
|
49
|
+
currency: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const FIXED_POINT_CENTS = 1_000_000n;
|
|
53
|
+
|
|
39
54
|
export function getKimiUsageToken(): string | null {
|
|
40
55
|
const credential = AuthStorage.create().get(PROVIDER_ID);
|
|
41
56
|
if (credential?.type === "oauth" && credential.access) return credential.access;
|
|
@@ -62,6 +77,82 @@ function getResetTime(record: Record<string, unknown>): string | undefined {
|
|
|
62
77
|
return undefined;
|
|
63
78
|
}
|
|
64
79
|
|
|
80
|
+
function toBigInt(value: unknown): bigint | null {
|
|
81
|
+
if (typeof value === "bigint") return value;
|
|
82
|
+
if (typeof value === "string" && /^-?\d+$/.test(value)) return BigInt(value);
|
|
83
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) return BigInt(value);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fixedPointToCents(value: bigint): bigint {
|
|
88
|
+
if (value > 0n && value < FIXED_POINT_CENTS) return 1n;
|
|
89
|
+
return (value + FIXED_POINT_CENTS / 2n) / FIXED_POINT_CENTS;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseMoney(value: unknown): { cents: bigint; currency: string } | null {
|
|
93
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
94
|
+
const record = value as Record<string, unknown>;
|
|
95
|
+
const cents = toBigInt(record.priceInCents);
|
|
96
|
+
if (cents === null) return null;
|
|
97
|
+
return {
|
|
98
|
+
cents,
|
|
99
|
+
currency: typeof record.currency === "string" ? record.currency : "",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseBoosterWallet(value: unknown): BoosterWalletInfo | null {
|
|
104
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
105
|
+
const record = value as Record<string, unknown>;
|
|
106
|
+
const balance = record.balance;
|
|
107
|
+
if (typeof balance !== "object" || balance === null || Array.isArray(balance)) return null;
|
|
108
|
+
const balanceRecord = balance as Record<string, unknown>;
|
|
109
|
+
if (balanceRecord.type !== "BOOSTER") return null;
|
|
110
|
+
const amount = toBigInt(balanceRecord.amount);
|
|
111
|
+
if (amount === null || amount < 0n) return null;
|
|
112
|
+
|
|
113
|
+
const amountLeft = toBigInt(balanceRecord.amountLeft);
|
|
114
|
+
const monthlyLimit = parseMoney(record.monthlyChargeLimit);
|
|
115
|
+
const monthlyUsed = parseMoney(record.monthlyUsed);
|
|
116
|
+
return {
|
|
117
|
+
balanceCents: amountLeft === null ? 0n : fixedPointToCents(amountLeft),
|
|
118
|
+
monthlyChargeLimitEnabled: record.monthlyChargeLimitEnabled === true,
|
|
119
|
+
monthlyChargeLimitCents: monthlyLimit?.cents ?? 0n,
|
|
120
|
+
monthlyUsedCents: monthlyUsed?.cents ?? 0n,
|
|
121
|
+
currency: monthlyLimit?.currency || monthlyUsed?.currency || "USD",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function formatCurrency(cents: bigint, currency: string): string {
|
|
126
|
+
const symbol =
|
|
127
|
+
currency.toUpperCase() === "USD" ? "$" : currency.toUpperCase() === "CNY" ? "¥" : "";
|
|
128
|
+
const sign = cents < 0n ? "-" : "";
|
|
129
|
+
const absolute = cents < 0n ? -cents : cents;
|
|
130
|
+
const amount = `${sign}${absolute / 100n}.${String(absolute % 100n).padStart(2, "0")}`;
|
|
131
|
+
return symbol ? `${symbol}${amount}` : `${amount} ${currency}`.trim();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatExtraUsage(info: BoosterWalletInfo): string[] {
|
|
135
|
+
const hasMonthlyLimit = info.monthlyChargeLimitEnabled && info.monthlyChargeLimitCents > 0n;
|
|
136
|
+
const lines = ["Extra Usage"];
|
|
137
|
+
if (hasMonthlyLimit) {
|
|
138
|
+
const used =
|
|
139
|
+
info.monthlyUsedCents < info.monthlyChargeLimitCents
|
|
140
|
+
? info.monthlyUsedCents
|
|
141
|
+
: info.monthlyChargeLimitCents;
|
|
142
|
+
const percent = Number(
|
|
143
|
+
(used * 100n + info.monthlyChargeLimitCents / 2n) / info.monthlyChargeLimitCents,
|
|
144
|
+
);
|
|
145
|
+
const barUnits = Number((used * 400n) / info.monthlyChargeLimitCents);
|
|
146
|
+
lines.push(`${quotaBar(barUnits, 400)} ${percent}% used`);
|
|
147
|
+
}
|
|
148
|
+
lines.push(`Used this month: ${formatCurrency(info.monthlyUsedCents, info.currency)}`);
|
|
149
|
+
lines.push(
|
|
150
|
+
`Monthly limit: ${hasMonthlyLimit ? formatCurrency(info.monthlyChargeLimitCents, info.currency) : "Unlimited"}`,
|
|
151
|
+
);
|
|
152
|
+
lines.push(`Balance: ${formatCurrency(info.balanceCents, info.currency)}`);
|
|
153
|
+
return lines;
|
|
154
|
+
}
|
|
155
|
+
|
|
65
156
|
export function parseUsageRow(value: unknown, fallbackLabel: string): UsageRow | null {
|
|
66
157
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
67
158
|
const record = value as Record<string, unknown>;
|
|
@@ -115,6 +206,12 @@ export function parseUsageSummary(payload: unknown, options: UsageFormatOptions
|
|
|
115
206
|
}
|
|
116
207
|
}
|
|
117
208
|
|
|
209
|
+
const extraUsage = parseBoosterWallet(record.boosterWallet);
|
|
210
|
+
if (extraUsage) {
|
|
211
|
+
if (lines.length > 0) lines.push("");
|
|
212
|
+
lines.push(...formatExtraUsage(extraUsage));
|
|
213
|
+
}
|
|
214
|
+
|
|
118
215
|
while (lines.at(-1) === "") lines.pop();
|
|
119
216
|
return lines.length === 0 ? "Usage: no usage data" : lines.join("\n");
|
|
120
217
|
}
|
|
@@ -140,7 +237,7 @@ function formatWindowLabel(value: unknown, fallbackLabel: string): string {
|
|
|
140
237
|
return `Current ${minutes}m window`;
|
|
141
238
|
}
|
|
142
239
|
|
|
143
|
-
export function
|
|
240
|
+
export function parseMembershipLevel(record: Record<string, unknown>): string | null {
|
|
144
241
|
const user = record.user;
|
|
145
242
|
if (typeof user !== "object" || user === null || Array.isArray(user)) return null;
|
|
146
243
|
const membership = (user as Record<string, unknown>).membership;
|
|
@@ -148,7 +245,12 @@ export function parseMembership(record: Record<string, unknown>): string | null
|
|
|
148
245
|
return null;
|
|
149
246
|
}
|
|
150
247
|
const level = (membership as Record<string, unknown>).level;
|
|
151
|
-
|
|
248
|
+
return typeof level === "string" && level ? level : null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function parseMembership(record: Record<string, unknown>): string | null {
|
|
252
|
+
const level = parseMembershipLevel(record);
|
|
253
|
+
if (!level) return null;
|
|
152
254
|
const name = MEMBERSHIP_LEVEL_NAMES[level];
|
|
153
255
|
return name ? `Membership: ${name} (${level})` : `Membership: ${level}`;
|
|
154
256
|
}
|
|
@@ -244,31 +346,49 @@ function fetchKimiUsage(token: string, signal: AbortSignal): Promise<Response> {
|
|
|
244
346
|
return fetch(buildKimiUsageUrl(), {
|
|
245
347
|
method: "GET",
|
|
246
348
|
headers: {
|
|
247
|
-
...
|
|
349
|
+
...getKimiProviderHeaders(),
|
|
248
350
|
Authorization: `Bearer ${token}`,
|
|
249
351
|
},
|
|
250
352
|
signal,
|
|
251
353
|
});
|
|
252
354
|
}
|
|
253
355
|
|
|
254
|
-
export async function
|
|
255
|
-
|
|
256
|
-
|
|
356
|
+
export async function fetchKimiUsageSnapshot(
|
|
357
|
+
options: { timeoutMs?: number; token?: string; refreshOnUnauthorized?: boolean } = {},
|
|
358
|
+
): Promise<KimiUsageSnapshot> {
|
|
359
|
+
const token = options.token ?? getKimiUsageToken();
|
|
360
|
+
if (!token) {
|
|
361
|
+
return {
|
|
362
|
+
summary: "Usage: missing credentials. Run /login kimi-coding.",
|
|
363
|
+
membershipLevel: null,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
257
366
|
|
|
258
367
|
const controller = new AbortController();
|
|
259
|
-
const timeout = setTimeout(() => controller.abort(), 15_000);
|
|
368
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 15_000);
|
|
260
369
|
try {
|
|
261
370
|
let response = await fetchKimiUsage(token, controller.signal);
|
|
262
|
-
if (response.status === 401) {
|
|
263
|
-
const refreshed = await refreshKimiAuthToken(token);
|
|
371
|
+
if (response.status === 401 && options.refreshOnUnauthorized !== false) {
|
|
372
|
+
const refreshed = await refreshKimiAuthToken(token, { signal: controller.signal });
|
|
264
373
|
if (refreshed) response = await fetchKimiUsage(refreshed, controller.signal);
|
|
265
374
|
}
|
|
266
|
-
if (!response.ok)
|
|
267
|
-
|
|
375
|
+
if (!response.ok) {
|
|
376
|
+
return { summary: `Usage: fetch failed (${response.status})`, membershipLevel: null };
|
|
377
|
+
}
|
|
378
|
+
const payload = (await response.json()) as unknown;
|
|
379
|
+
const membershipLevel =
|
|
380
|
+
typeof payload === "object" && payload !== null && !Array.isArray(payload)
|
|
381
|
+
? parseMembershipLevel(payload as Record<string, unknown>)
|
|
382
|
+
: null;
|
|
383
|
+
return { summary: parseUsageSummary(payload), membershipLevel };
|
|
268
384
|
} catch (error) {
|
|
269
385
|
const message = error instanceof Error ? error.message : String(error);
|
|
270
|
-
return `Usage: fetch failed (${message})
|
|
386
|
+
return { summary: `Usage: fetch failed (${message})`, membershipLevel: null };
|
|
271
387
|
} finally {
|
|
272
388
|
clearTimeout(timeout);
|
|
273
389
|
}
|
|
274
390
|
}
|
|
391
|
+
|
|
392
|
+
export async function fetchKimiUsageSummary(): Promise<string> {
|
|
393
|
+
return (await fetchKimiUsageSnapshot()).summary;
|
|
394
|
+
}
|