pi-provider-kimi-code 0.6.12 → 0.6.14

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/src/usage.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { PROVIDER_ID, getBaseUrl } from "./constants.ts";
1
+ import { PROVIDER_ID, getBaseUrl, normalizeKimiBaseV1 } from "./constants.ts";
2
2
  import { getKimiProviderHeaders } from "./device.ts";
3
3
  import { readStoredOAuthCredential, refreshKimiAuthToken } from "./oauth.ts";
4
4
 
@@ -51,6 +51,13 @@ function toNumber(value: unknown): number | null {
51
51
  return Number.isFinite(number) ? number : null;
52
52
  }
53
53
 
54
+ function toOptionalNumber(value: unknown): number | null {
55
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
56
+ if (typeof value !== "string" || !value.trim()) return null;
57
+ const number = Number(value);
58
+ return Number.isFinite(number) ? number : null;
59
+ }
60
+
54
61
  function toStringValue(value: unknown): string | undefined {
55
62
  if (typeof value === "string" && value.trim()) return value;
56
63
  if (typeof value === "number" && Number.isFinite(value)) return String(value);
@@ -141,6 +148,85 @@ function formatExtraUsage(info: BoosterWalletInfo): string[] {
141
148
  return lines;
142
149
  }
143
150
 
151
+ // Quota model (upstream kimi-code #3787): the platform /usages payload now
152
+ // serves per-window used ratios under `usages` instead of absolute used/limit
153
+ // rows. Keys are snake_case (`limit_5h`, `limit_7d`, `limit_month_total`,
154
+ // `limit_month_code`); each entry carries `used_ratio` (0..1, sometimes a
155
+ // decimal string) and an optional `reset_time`. Rows are presence-driven —
156
+ // the backend omits windows the plan does not have. The legacy `usage` /
157
+ // `limits` rows are still served alongside during the transition, but the
158
+ // legacy wire is gone from kap-server, so quota rows win when present.
159
+ interface QuotaEntry {
160
+ usedRatio: number;
161
+ resetTime?: string;
162
+ }
163
+
164
+ const QUOTA_WINDOWS = [
165
+ ["limit_5h", "5h limit"],
166
+ ["limit_7d", "Weekly limit"],
167
+ ["limit_month_total", "Monthly limit"],
168
+ ] as const;
169
+
170
+ const QUOTA_MONTH_CODE_KEY = "limit_month_code";
171
+
172
+ function isRecordValue(value: unknown): value is Record<string, unknown> {
173
+ return typeof value === "object" && value !== null && !Array.isArray(value);
174
+ }
175
+
176
+ function toRatio(value: unknown): number | undefined {
177
+ if (typeof value === "number") {
178
+ return Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : undefined;
179
+ }
180
+ if (typeof value !== "string" || !value.trim()) return undefined;
181
+ const parsed = Number(value);
182
+ return Number.isFinite(parsed) ? Math.max(0, Math.min(1, parsed)) : undefined;
183
+ }
184
+
185
+ function parseQuotaEntry(value: unknown): QuotaEntry | undefined {
186
+ if (!isRecordValue(value)) return undefined;
187
+ const usedRatio = toRatio(value.used_ratio);
188
+ if (usedRatio === undefined) return undefined;
189
+ const resetTime = toStringValue(value.reset_time);
190
+ return {
191
+ usedRatio,
192
+ ...(resetTime ? { resetTime } : {}),
193
+ };
194
+ }
195
+
196
+ function parseQuotaWindows(record: Record<string, unknown>): Map<string, QuotaEntry> {
197
+ const windows = new Map<string, QuotaEntry>();
198
+ const usages = record.usages;
199
+ if (!isRecordValue(usages)) return windows;
200
+ for (const key of [...QUOTA_WINDOWS.map(([key]) => key), QUOTA_MONTH_CODE_KEY]) {
201
+ const entry = parseQuotaEntry(usages[key]);
202
+ if (entry) windows.set(key, entry);
203
+ }
204
+ return windows;
205
+ }
206
+
207
+ function formatQuotaRow(label: string, entry: QuotaEntry, options: UsageFormatOptions): string {
208
+ const percent = Math.min(100, Math.max(0, Math.ceil(entry.usedRatio * 100)));
209
+ const lines = [label, `${quotaBar(entry.usedRatio * 100, 100)} ${percent}% used`];
210
+ const reset = formatResetTime(entry.resetTime, options);
211
+ if (reset) lines.push(`Resets ${reset}`);
212
+ return lines.join("\n");
213
+ }
214
+
215
+ // The kimi/code split of the monthly quota on the new plan: `code` is the
216
+ // code-typed share of the monthly total as served, `kimi` the remainder
217
+ // (mirrors upstream monthlyBreakdown in apps/kimi-code usage-format).
218
+ function formatMonthlyBreakdown(
219
+ total: QuotaEntry,
220
+ windows: Map<string, QuotaEntry>,
221
+ ): string | null {
222
+ const code = windows.get(QUOTA_MONTH_CODE_KEY);
223
+ if (!code) return null;
224
+ const codePercent = Math.min(100, Math.max(0, Math.ceil(code.usedRatio * 100)));
225
+ const totalPercent = Math.min(100, Math.max(0, Math.ceil(total.usedRatio * 100)));
226
+ const kimiPercent = Math.max(0, totalPercent - codePercent);
227
+ return `kimi ${kimiPercent}% · code ${codePercent}%`;
228
+ }
229
+
144
230
  export function parseUsageRow(value: unknown, fallbackLabel: string): UsageRow | null {
145
231
  if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
146
232
  const record = value as Record<string, unknown>;
@@ -164,11 +250,28 @@ export function parseUsageSummary(payload: unknown, options: UsageFormatOptions
164
250
 
165
251
  const record = payload as Record<string, unknown>;
166
252
  const lines: string[] = [];
167
- const summary = parseUsageRow(record.usage, "Current week");
253
+
254
+ const quotaWindows = parseQuotaWindows(record);
255
+ // A code-split entry without a monthly total renders no row of its own; it
256
+ // only feeds the breakdown line, so it must not suppress the legacy rows.
257
+ const hasQuotaRows = QUOTA_WINDOWS.some(([key]) => quotaWindows.has(key));
258
+ if (hasQuotaRows) {
259
+ for (const [key, label] of QUOTA_WINDOWS) {
260
+ const entry = quotaWindows.get(key);
261
+ if (!entry) continue;
262
+ if (lines.length > 0) lines.push("");
263
+ lines.push(...formatQuotaRow(label, entry, options).split("\n"));
264
+ const breakdown =
265
+ key === "limit_month_total" ? formatMonthlyBreakdown(entry, quotaWindows) : null;
266
+ if (breakdown) lines.push(breakdown);
267
+ }
268
+ }
269
+
270
+ const summary = hasQuotaRows ? null : parseUsageRow(record.usage, "Current week");
168
271
  if (summary)
169
272
  lines.push(formatUsageRow({ ...summary, label: normalizeUsageLabel(summary.label) }, options));
170
273
 
171
- if (Array.isArray(record.limits)) {
274
+ if (!hasQuotaRows && Array.isArray(record.limits)) {
172
275
  for (const [index, item] of record.limits.entries()) {
173
276
  const itemRecord =
174
277
  typeof item === "object" && item !== null && !Array.isArray(item)
@@ -307,8 +410,7 @@ function partialBlock(value: number): string {
307
410
  }
308
411
 
309
412
  export function buildKimiUsageUrl(baseUrl = getBaseUrl("openai")): string {
310
- const normalized = baseUrl.replace(/\/+$/, "");
311
- return normalized.endsWith("/v1") ? `${normalized}/usages` : `${normalized}/v1/usages`;
413
+ return `${normalizeKimiBaseV1(baseUrl)}/usages`;
312
414
  }
313
415
 
314
416
  function fetchKimiUsage(token: string, signal: AbortSignal): Promise<Response> {
@@ -383,6 +485,12 @@ export interface KimiUserInfo {
383
485
  userLevel: number;
384
486
  userLevelName: string;
385
487
  email?: string;
488
+ /**
489
+ * Plan generation from the managed /me profile (upstream #3921). upstream
490
+ * gates feedback surveys and plan-dependent surfaces by it; the monthly
491
+ * kimi/code quota split only exists on newer goods versions.
492
+ */
493
+ goodsVersion?: number;
386
494
  }
387
495
 
388
496
  export function parseKimiUserInfo(payload: unknown): KimiUserInfo | null {
@@ -391,12 +499,14 @@ export function parseKimiUserInfo(payload: unknown): KimiUserInfo | null {
391
499
  const userId = toStringValue(record.user_id);
392
500
  if (!userId) return null;
393
501
  const email = toStringValue(record.email);
502
+ const goodsVersion = toOptionalNumber(record.goods_version);
394
503
  return {
395
504
  userId,
396
505
  nickname: toStringValue(record.nickname) ?? "",
397
506
  userLevel: toNumber(record.user_level) ?? 0,
398
507
  userLevelName: toStringValue(record.user_level_name) ?? "",
399
508
  ...(email ? { email } : {}),
509
+ ...(goodsVersion !== null ? { goodsVersion } : {}),
400
510
  };
401
511
  }
402
512
 
@@ -412,8 +522,7 @@ export function formatKimiUserInfo(info: KimiUserInfo): string {
412
522
  }
413
523
 
414
524
  export function buildKimiUserInfoUrl(baseUrl = getBaseUrl("openai")): string {
415
- const normalized = baseUrl.replace(/\/+$/, "");
416
- return normalized.endsWith("/v1") ? `${normalized}/me` : `${normalized}/v1/me`;
525
+ return `${normalizeKimiBaseV1(baseUrl)}/me`;
417
526
  }
418
527
 
419
528
  function fetchKimiUserInfo(token: string, signal: AbortSignal): Promise<Response> {