feat(usage): show Kimi Coding Extra Usage (#10712)

Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
This commit is contained in:
Xiangzhe
2026-08-20 17:28:37 +08:00
committed by GitHub
parent 1accabeb4e
commit 82e5afed6b
20 changed files with 1181 additions and 71 deletions

View File

@@ -0,0 +1 @@
- **feat(usage):** show Kimi Coding's fixed-order Code 5-hour/7-day quota windows plus Extra Usage status, balance, monthly spend/limit, and the official Additional Credits link on Dashboard → Quota cards.

View File

@@ -9,12 +9,16 @@
*/
import { safePercentage } from "@/shared/utils/formatting";
import {
KIMI_CODE_ADDITIONAL_CREDITS_URL,
type KimiBillingStatus,
} from "@/shared/utils/kimiBilling";
import {
buildKimiCodeIdentityHeaders,
getKimiCodeCliUserAgent,
} from "../../config/providers/registry/kimi/coding/runtime.ts";
import { toRecord, toNumber } from "./scalars.ts";
import { type UsageQuota, parseResetTime } from "./quota.ts";
import { createQuotaFromUsage, type UsageQuota, parseResetTime } from "./quota.ts";
type JsonRecord = Record<string, unknown>;
@@ -25,6 +29,145 @@ const KIMI_CONFIG = {
apiVersion: "2023-06-01",
};
const KIMI_BOOSTER_FIXED_POINT_PER_CENT = 1_000_000;
function toInteger(value: unknown): number | null {
const parsed = toNumber(value, Number.NaN);
return Number.isFinite(parsed) ? Math.trunc(parsed) : null;
}
function fixedPointToCents(value: number): number {
const cents = value / KIMI_BOOSTER_FIXED_POINT_PER_CENT;
if (cents > 0 && cents < 1) return 1;
return Math.round(cents);
}
function parseKimiMoney(value: unknown): { cents: number; currency: string } | null {
const money = toRecord(value);
const cents = toInteger(money.priceInCents);
const currency = money.currency;
if (
cents === null ||
cents < 0 ||
typeof currency !== "string" ||
!/^[A-Za-z]{3}$/.test(currency)
) {
return null;
}
return { cents, currency: currency.toUpperCase() };
}
function parseKimiExtraUsageStatus(value: unknown): KimiBillingStatus["extraUsageStatus"] {
switch (value) {
case "STATUS_ACTIVE":
return "enabled";
case "STATUS_DISABLED":
return "disabled";
case "STATUS_FROZEN":
return "frozen";
default:
return "unavailable";
}
}
function parseKimiBoosterWallet(value: unknown): KimiBillingStatus | null {
const wallet = toRecord(value);
const balance = toRecord(wallet.balance);
if (balance.type !== "BOOSTER") return null;
const amount = toInteger(balance.amount);
const amountLeft = toInteger(balance.amountLeft);
const monthlyLimit = parseKimiMoney(wallet.monthlyChargeLimit);
const monthlyUsed = parseKimiMoney(wallet.monthlyUsed);
const autoRefillCharge = parseKimiMoney(wallet.autoRefillCharge);
const autoRefillThreshold = parseKimiMoney(wallet.autoRefillThreshold);
const extraUsageStatus = parseKimiExtraUsageStatus(wallet.status);
const hasWalletEvidence =
(amount !== null && amount > 0) ||
amountLeft !== null ||
monthlyLimit !== null ||
monthlyUsed !== null ||
extraUsageStatus !== "unavailable";
if (!hasWalletEvidence) return null;
const currency =
monthlyLimit?.currency ??
monthlyUsed?.currency ??
autoRefillCharge?.currency ??
autoRefillThreshold?.currency ??
"USD";
return {
currency,
// Proto JSON omits numeric zero values. Production therefore returns a
// BOOSTER balance record without amount/amountLeft when the preserved
// balance is exactly zero; treat that as an explicit zero, not unknown.
extraCreditsMinorUnits:
amountLeft === null || amountLeft < 0 ? 0 : fixedPointToCents(amountLeft),
monthlyUsedMinorUnits: monthlyUsed?.cents ?? 0,
monthlyLimitEnabled: wallet.monthlyChargeLimitEnabled === true,
monthlyLimitMinorUnits: monthlyLimit?.cents ?? 0,
extraUsageStatus,
additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL,
};
}
function buildKimiBillingStatus(value: unknown): KimiBillingStatus {
return (
parseKimiBoosterWallet(value) ?? {
currency: "USD",
extraUsageStatus: "unavailable",
additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL,
}
);
}
function optionalNumber(value: unknown): number | null {
if (typeof value !== "number" && typeof value !== "string") return null;
const parsed = toNumber(value, Number.NaN);
return Number.isFinite(parsed) ? parsed : null;
}
function createKimiCountQuota(value: unknown): UsageQuota | null {
const detail = toRecord(value);
const limit = optionalNumber(detail.limit ?? detail.Limit);
if (limit === null || limit <= 0) return null;
const reportedUsed = optionalNumber(detail.used ?? detail.Used);
const reportedRemaining = optionalNumber(detail.remaining ?? detail.Remaining);
const used = reportedUsed ?? (reportedRemaining === null ? 0 : limit - reportedRemaining);
return createQuotaFromUsage(used, limit, detail.resetTime ?? detail.reset_at ?? detail.resetAt);
}
type KimiWindowLabel = { key: string; displayName: string };
function normalizeKimiWindow(value: unknown, fallbackIndex: number): KimiWindowLabel {
const window = toRecord(value);
const duration = optionalNumber(window.duration);
const timeUnit = window.timeUnit;
if (duration !== null && duration > 0) {
if (timeUnit === "TIME_UNIT_MINUTE" && duration % 60 === 0) {
const hours = duration / 60;
return { key: `${hours}h`, displayName: `Code · ${hours}h` };
}
if (timeUnit === "TIME_UNIT_HOUR") {
return { key: `${duration}h`, displayName: `Code · ${duration}h` };
}
if (timeUnit === "TIME_UNIT_DAY") {
return { key: `${duration}d`, displayName: `Code · ${duration}d` };
}
if (timeUnit === "TIME_UNIT_WEEK") {
return { key: `${duration}w`, displayName: `Code · ${duration}w` };
}
if (timeUnit === "TIME_UNIT_MINUTE") {
return { key: `${duration}m`, displayName: `Code · ${duration}m` };
}
}
return { key: `limit_${fallbackIndex}`, displayName: `Code · Limit ${fallbackIndex}` };
}
/**
* Map Kimi membership level to display name
* LEVEL_BASIC = Moderato, LEVEL_INTERMEDIATE = Allegretto,
@@ -100,52 +243,38 @@ export async function getKimiUsage(
const quotas: Record<string, UsageQuota> = {};
const dataObj = toRecord(data);
const billing = buildKimiBillingStatus(dataObj.boosterWallet);
// Parse Kimi usage response format
// Format: { user: {...}, usage: { limit: "100", used: "92", remaining: "8", resetTime: "..." }, limits: [...] }
const usageObj = toRecord(dataObj.usage);
// Check for Kimi's actual usage fields (strings, not numbers)
const usageLimit = toNumber(usageObj.limit || usageObj.Limit, 0);
const usageUsed = toNumber(usageObj.used || usageObj.Used, 0);
const usageRemaining = toNumber(usageObj.remaining || usageObj.Remaining, 0);
const usageResetTime =
usageObj.resetTime || usageObj.ResetTime || usageObj.reset_at || usageObj.resetAt;
if (usageLimit > 0) {
const percentRemaining = usageLimit > 0 ? (usageRemaining / usageLimit) * 100 : 0;
quotas["Weekly"] = {
used: usageUsed,
total: usageLimit,
remaining: usageRemaining,
remainingPercentage: percentRemaining,
resetAt: parseResetTime(usageResetTime),
unlimited: false,
};
// The managed Kimi Code API reports the Code 7-day quota in `usage`.
// The website's separate shared-membership total/Kimi split comes from a
// Web-session-only endpoint and cannot be read with a Coding OAuth token.
const weeklyQuota = createKimiCountQuota(dataObj.usage);
if (weeklyQuota) {
quotas.code_7d = { ...weeklyQuota, displayName: "Code · 7d" };
}
// Also parse limits array for rate limits
// Each limits[] item is an independent rolling window. Preserve all of
// them with deterministic window-derived keys instead of overwriting one
// generic `Ratelimit` row.
const limitsArray = Array.isArray(dataObj.limits) ? dataObj.limits : [];
for (let i = 0; i < limitsArray.length; i++) {
const limitItem = toRecord(limitsArray[i]);
const window = toRecord(limitItem.window);
const detail = toRecord(limitItem.detail);
const quota = createKimiCountQuota(limitItem.detail);
if (!quota) continue;
const limit = toNumber(detail.limit || detail.Limit, 0);
const remaining = toNumber(detail.remaining || detail.Remaining, 0);
const resetTime = detail.resetTime || detail.reset_at || detail.resetAt;
if (limit > 0) {
quotas["Ratelimit"] = {
used: limit - remaining,
total: limit,
remaining,
remainingPercentage: limit > 0 ? (remaining / limit) * 100 : 0,
resetAt: parseResetTime(resetTime),
unlimited: false,
};
}
const normalized = normalizeKimiWindow(limitItem.window, i + 1);
const baseKey = `code_${normalized.key}`;
let key = baseKey;
let suffix = 2;
while (key in quotas) key = `${baseKey}_${suffix++}`;
const reportedName =
typeof limitItem.name === "string" && limitItem.name.trim() ? limitItem.name.trim() : null;
const displayName = reportedName
? /^code\b/i.test(reportedName)
? reportedName
: `Code · ${reportedName}`
: normalized.displayName;
quotas[key] = { ...quota, displayName };
}
// Check for quota windows (Claude-like format with utilization) as fallback
@@ -189,6 +318,7 @@ export async function getKimiUsage(
return {
plan: planName || "Kimi Coding",
quotas,
billing,
};
}
@@ -199,6 +329,7 @@ export async function getKimiUsage(
return {
plan: planName || "Kimi Coding",
message: "Kimi Coding connected. Usage tracked per request.",
billing,
};
} catch (error) {
return {

View File

@@ -2,7 +2,10 @@
import { useMemo, useState } from "react";
import Card from "@/shared/components/Card";
import type { GrokBillingStatus } from "@/shared/utils/grokBilling";
import {
isProviderBillingProvider,
type ProviderBillingStatus,
} from "@/shared/utils/providerBilling";
import { pickDisplayValue } from "@/shared/utils/maskEmail";
import {
normalizePlanTier,
@@ -35,8 +38,8 @@ interface QuotaCardProps {
quotas?: any[];
plan?: string | null;
message?: string | null;
billing?: GrokBillingStatus | null;
raw?: { billing?: GrokBillingStatus | null };
billing?: ProviderBillingStatus | null;
raw?: { billing?: ProviderBillingStatus | null };
stale?: { since?: string; reason?: string } | null;
}
| undefined;
@@ -151,7 +154,9 @@ export default function QuotaCard({
error={error}
message={quota?.message ?? null}
billing={
connection.provider === "grok-cli" ? (quota?.billing ?? quota?.raw?.billing) : null
isProviderBillingProvider(connection.provider)
? (quota?.billing ?? quota?.raw?.billing)
: null
}
refreshedAt={displayRefreshedAt}
hasStaleData={hasStaleData}

View File

@@ -2,7 +2,13 @@
import { useMemo, useState } from "react";
import { useLocale, useTranslations } from "next-intl";
import { buildGrokBillingCardRows, type GrokBillingStatus } from "@/shared/utils/grokBilling";
import { buildGrokBillingCardRows } from "@/shared/utils/grokBilling";
import { buildKimiBillingCardRows } from "@/shared/utils/kimiBilling";
import {
isKimiBillingStatus,
isProviderBillingProvider,
type ProviderBillingStatus,
} from "@/shared/utils/providerBilling";
import {
formatCountdown,
formatQuotaLabel,
@@ -27,19 +33,23 @@ const CURRENCY_SYMBOLS: Record<string, string> = {
const DEFAULT_VISIBLE_ROWS = 3;
function GrokBillingDetails({ billing }: { billing: GrokBillingStatus }) {
function ProviderBillingDetails({ billing }: { billing: ProviderBillingStatus }) {
const t = useTranslations("usage");
const locale = useLocale();
const rows = buildGrokBillingCardRows(billing, locale, (key, fallback) =>
translateUsageOrFallback(t, key, fallback)
);
const rows = isKimiBillingStatus(billing)
? buildKimiBillingCardRows(billing, locale, (key, fallback) =>
translateUsageOrFallback(t, key, fallback)
)
: buildGrokBillingCardRows(billing, locale, (key, fallback) =>
translateUsageOrFallback(t, key, fallback)
);
return (
<div className="flex flex-col gap-1.5 border-t border-border/40 pt-2 text-[11px] text-text-main">
{rows.map((row) =>
row.kind === "link" ? (
<a
key={row.kind}
key={`${row.kind}-${row.label}`}
href={row.href}
target={row.target}
rel={row.rel}
@@ -50,7 +60,7 @@ function GrokBillingDetails({ billing }: { billing: GrokBillingStatus }) {
</a>
) : (
<div
key={row.kind}
key={`${row.kind}-${row.label}`}
className={`flex justify-between gap-2 ${
row.kind === "status" ? "items-start" : "items-center"
}`}
@@ -77,8 +87,8 @@ export function sortQuotasByRemaining(quotas: any[]): any[] {
/**
* Pure helper — resolves the display order for a provider's quotas.
* Providers with a deterministic fixed-window order (codex, glm family — see
* quotaParsing.ts's sortCodexOrder()/sortGlmOrder()) keep the order
* Providers with a deterministic fixed-window order (Codex, GLM family,
* Kimi Coding — see quotaParsing.ts) keep the order
* parseQuotaData() already established. Every other provider still gets the
* remaining-percentage sort. Fixes #6687 (bars re-sorted by % undid the fixed
* session/weekly order).
@@ -115,7 +125,7 @@ interface Props {
loading: boolean;
error: string | null;
message?: string | null;
billing?: GrokBillingStatus | null;
billing?: ProviderBillingStatus | null;
refreshedAt?: string;
hasStaleData: boolean;
onRefresh: () => void;
@@ -357,7 +367,9 @@ export default function QuotaCardExpanded({
</div>
)}
{providerId === "grok-cli" && billing && <GrokBillingDetails billing={billing} />}
{isProviderBillingProvider(providerId) && billing && (
<ProviderBillingDetails billing={billing} />
)}
{hiddenQuotaRows.length > 0 && (
<div className="flex flex-wrap items-center gap-1 border-t border-border/40 pt-1.5 text-[10px] text-text-muted">

View File

@@ -10,15 +10,16 @@ const CODEX_QUOTA_ORDER: Record<string, number> = {
banked_reset_credits: 4,
};
const GLM_FAMILY_PROVIDERS = ["glm", "glm-cn", "glmt", "opencode-go"];
const KIMI_CODING_PROVIDERS = ["kimi-coding", "kimi-coding-apikey"];
/**
* Providers whose quotas already get a deterministic fixed-window order from
* sortGlmOrder()/sortCodexOrder() below. Display layers (e.g. QuotaCardExpanded)
* Providers whose quotas already get a deterministic fixed-window order below
* (Codex, GLM family, and Kimi Coding). Display layers (e.g. QuotaCardExpanded)
* must not re-sort these by remaining percentage, or they undo this order (#6687).
*/
export function hasFixedQuotaOrder(providerId: string | undefined): boolean {
const id = String(providerId || "").toLowerCase();
return id === "codex" || GLM_FAMILY_PROVIDERS.includes(id);
return id === "codex" || GLM_FAMILY_PROVIDERS.includes(id) || KIMI_CODING_PROVIDERS.includes(id);
}
function quotaEntries(data: any): Array<[string, any]> {
@@ -269,6 +270,19 @@ function sortCodexOrder(providerId: string, quotas: any[]) {
quotas.sort((a, b) => (CODEX_QUOTA_ORDER[a.name] ?? 99) - (CODEX_QUOTA_ORDER[b.name] ?? 99));
}
function sortKimiOrder(providerId: string, quotas: any[]) {
if (!KIMI_CODING_PROVIDERS.includes(providerId)) return;
const rank = (name: string) => {
if (/^code_5h(?:_|$)/.test(name)) return 0;
if (/^code_7d(?:_|$)/.test(name)) return 1;
return 99;
};
quotas.sort((a, b) => {
const rankDiff = rank(String(a.name)) - rank(String(b.name));
return rankDiff || String(a.name).localeCompare(String(b.name));
});
}
export function parseQuotaData(provider: string | undefined, data: any) {
if (!data || typeof data !== "object") return [];
const providerId = String(provider || "").toLowerCase();
@@ -278,6 +292,7 @@ export function parseQuotaData(provider: string | undefined, data: any) {
sortProviderModelOrder(provider, normalizedQuotas);
sortGlmOrder(providerId, normalizedQuotas);
sortCodexOrder(providerId, normalizedQuotas);
sortKimiOrder(providerId, normalizedQuotas);
return normalizedQuotas;
} catch (error) {
console.error(`Error parsing quota data for ${provider}:`, error);

View File

@@ -392,8 +392,8 @@ const STATUS_ORDER: Record<"critical" | "alert" | "ok", number> = {
export function topQuotas(quotas: any[], n = 3, providerId?: string): any[] {
const filtered = quotas.filter(Boolean);
// Providers with a deterministic fixed-window order (codex, glm family — see
// quotaParsing.ts's sortCodexOrder()/sortGlmOrder()) must keep the order
// Providers with a deterministic fixed-window order (Codex, GLM family,
// Kimi Coding — see quotaParsing.ts) must keep the order
// parseQuotaData() already established rather than being re-sorted by
// status/remaining-%, which would undo it (#6687's collapsed-card sibling, #7764).
if (hasFixedQuotaOrder(providerId)) {

View File

@@ -9098,6 +9098,16 @@
"grokAutoTopUpMax": "max",
"grokAutoTopUpMonth": "month",
"grokAdditionalCredits": "Additional Credits",
"kimiExtraUsageCredits": "Extra Usage Credits",
"kimiExtraUsage": "Extra Usage",
"kimiExtraUsageEnabled": "Enabled",
"kimiExtraUsageDisabled": "Disabled",
"kimiExtraUsageFrozen": "Frozen",
"kimiExtraUsageUnavailable": "Unavailable",
"kimiMonthlyUsed": "Used this month",
"kimiMonthlyLimit": "Monthly limit",
"kimiMonthlyLimitUnlimited": "Unlimited",
"kimiAdditionalCredits": "Additional Credits",
"loggerTab": "Logger",
"proxyTab": "Proxy",
"budgetManagement": "Budget Management",

View File

@@ -9090,6 +9090,16 @@
"grokAutoTopUpMax": "máximo",
"grokAutoTopUpMonth": "mês",
"grokAdditionalCredits": "Créditos adicionais",
"kimiExtraUsageCredits": "Créditos de uso extra",
"kimiExtraUsage": "Uso extra",
"kimiExtraUsageEnabled": "Ativado",
"kimiExtraUsageDisabled": "Desativado",
"kimiExtraUsageFrozen": "Congelado",
"kimiExtraUsageUnavailable": "Indisponível",
"kimiMonthlyUsed": "Usado neste mês",
"kimiMonthlyLimit": "Limite mensal",
"kimiMonthlyLimitUnlimited": "Ilimitado",
"kimiAdditionalCredits": "Créditos adicionais",
"loggerTab": "Logger",
"proxyTab": "Proxy",
"budgetManagement": "Gerenciamento de Orçamento",

View File

@@ -9098,6 +9098,16 @@
"grokAutoTopUpMax": "tối đa",
"grokAutoTopUpMonth": "tháng",
"grokAdditionalCredits": "Tín dụng bổ sung",
"kimiExtraUsageCredits": "Tín dụng sử dụng bổ sung",
"kimiExtraUsage": "Sử dụng bổ sung",
"kimiExtraUsageEnabled": "Đã bật",
"kimiExtraUsageDisabled": "Đã tắt",
"kimiExtraUsageFrozen": "Đã đóng băng",
"kimiExtraUsageUnavailable": "Không khả dụng",
"kimiMonthlyUsed": "Đã dùng trong tháng này",
"kimiMonthlyLimit": "Giới hạn hàng tháng",
"kimiMonthlyLimitUnlimited": "Không giới hạn",
"kimiAdditionalCredits": "Tín dụng bổ sung",
"loggerTab": "Logger",
"proxyTab": "Proxy",
"budgetManagement": "Quản lý ngân sách",

View File

@@ -9077,6 +9077,16 @@
"grokAutoTopUpMax": "最大",
"grokAutoTopUpMonth": "月",
"grokAdditionalCredits": "额外的致谢",
"kimiExtraUsageCredits": "加油包余额",
"kimiExtraUsage": "额度加油包",
"kimiExtraUsageEnabled": "已开启",
"kimiExtraUsageDisabled": "已关闭",
"kimiExtraUsageFrozen": "已冻结",
"kimiExtraUsageUnavailable": "不可用",
"kimiMonthlyUsed": "本月已用",
"kimiMonthlyLimit": "每月限额",
"kimiMonthlyLimitUnlimited": "无限制",
"kimiAdditionalCredits": "充值加油包",
"loggerTab": "记录器",
"proxyTab": "代理",
"budgetManagement": "预算管理",

View File

@@ -9077,6 +9077,16 @@
"grokAutoTopUpMax": "最大",
"grokAutoTopUpMonth": "月份",
"grokAdditionalCredits": "額外的致謝",
"kimiExtraUsageCredits": "加油包餘額",
"kimiExtraUsage": "額度加油包",
"kimiExtraUsageEnabled": "已開啟",
"kimiExtraUsageDisabled": "已關閉",
"kimiExtraUsageFrozen": "已凍結",
"kimiExtraUsageUnavailable": "無法使用",
"kimiMonthlyUsed": "本月已用",
"kimiMonthlyLimit": "每月限額",
"kimiMonthlyLimitUnlimited": "無限制",
"kimiAdditionalCredits": "儲值加油包",
"loggerTab": "記錄器",
"proxyTab": "代理",
"budgetManagement": "預算管理",

View File

@@ -1,4 +1,7 @@
import { sanitizeGrokBillingStatus, type GrokBillingStatus } from "@/shared/utils/grokBilling";
import {
sanitizeProviderBillingStatus,
type ProviderBillingStatus,
} from "@/shared/utils/providerBilling";
import { getDbInstance, isBuildPhase, isCloud } from "./core";
type JsonRecord = Record<string, unknown>;
@@ -26,7 +29,7 @@ export interface ProviderLimitsCacheEntry {
fetchedAt: string;
source?: string | null;
bankedResetCredits?: number;
billing?: GrokBillingStatus;
billing?: ProviderBillingStatus;
}
const PROVIDER_LIMITS_CACHE_NAMESPACE = "providerLimitsCache";
@@ -45,7 +48,7 @@ function toRecord(value: unknown): JsonRecord | null {
function sanitizeCacheEntryForStorage(entry: ProviderLimitsCacheEntry): ProviderLimitsCacheEntry {
const { billing: rawBilling, ...rest } = entry;
const billing = sanitizeGrokBillingStatus(rawBilling);
const billing = sanitizeProviderBillingStatus(rawBilling);
return billing ? { ...rest, billing } : rest;
}
@@ -58,7 +61,7 @@ function normalizeCacheEntry(value: unknown): ProviderLimitsCacheEntry | null {
if (!fetchedAt) return null;
const bankedResetCredits = Number(record.bankedResetCredits);
const billing = sanitizeGrokBillingStatus(record.billing);
const billing = sanitizeProviderBillingStatus(record.billing);
return {
quotas: toRecord(record.quotas),

View File

@@ -1,5 +1,6 @@
import type { ProviderLimitsCacheEntry } from "@/lib/db/providerLimits";
import { sanitizeGrokBillingStatus } from "@/shared/utils/grokBilling";
import { sanitizeProviderBillingStatus } from "@/shared/utils/providerBilling";
import { GROK_BUILD_ADDITIONAL_CREDITS_URL } from "@/shared/utils/grokBilling";
const GROK_CLI_PROVIDER = "grok-cli";
@@ -26,7 +27,7 @@ export function toProviderLimitsCacheEntry(
fetchedAt,
source,
bankedResetCredits: Number.isFinite(bankedResetCredits) ? bankedResetCredits : undefined,
billing: sanitizeGrokBillingStatus(usage.billing),
billing: sanitizeProviderBillingStatus(usage.billing),
};
}
@@ -44,14 +45,21 @@ export function mergeProviderLimitsCacheEntry(
if (provider !== GROK_CLI_PROVIDER) return next;
const nextBilling = next.billing;
const previousAutoTopUp = previous.billing?.autoTopUp;
if (!nextBilling || nextBilling.autoTopUp.available || !previousAutoTopUp) return next;
const previousBilling = previous.billing;
if (
!nextBilling ||
nextBilling.additionalCreditsUrl !== GROK_BUILD_ADDITIONAL_CREDITS_URL ||
nextBilling.autoTopUp.available ||
!previousBilling ||
previousBilling.additionalCreditsUrl !== GROK_BUILD_ADDITIONAL_CREDITS_URL
)
return next;
return {
...next,
billing: {
...nextBilling,
autoTopUp: previousAutoTopUp,
autoTopUp: previousBilling.autoTopUp,
},
};
}

View File

@@ -0,0 +1,199 @@
/**
* Public Dashboard contract for Kimi Coding Extra Usage (额度加油包).
*
* The existing read-only `GET /coding/v1/usages` response carries both the
* Code quota windows and `boosterWallet`. Only the strictly whitelisted fields
* below may cross the Provider Limits cache/UI boundary.
*/
export const KIMI_CODE_ADDITIONAL_CREDITS_URL =
"https://www.kimi.com/membership/subscription?tab=quota&aff=omniroute";
type KimiExtraUsageStatus = "enabled" | "disabled" | "frozen" | "unavailable";
export interface KimiBillingStatus {
/** ISO 4217 currency reported by the wallet money wrappers. */
currency: string;
/** Remaining Extra Usage balance in cents. */
extraCreditsMinorUnits?: number;
/** Extra Usage spend so far this calendar month, in cents. */
monthlyUsedMinorUnits?: number;
/** Whether the member enabled a monthly spending cap. */
monthlyLimitEnabled?: boolean;
/** Monthly spending cap in cents; 0/absent means unlimited. */
monthlyLimitMinorUnits?: number;
extraUsageStatus: KimiExtraUsageStatus;
additionalCreditsUrl: typeof KIMI_CODE_ADDITIONAL_CREDITS_URL;
}
type KimiBillingTranslationKey =
| "kimiExtraUsageCredits"
| "kimiExtraUsage"
| "kimiExtraUsageEnabled"
| "kimiExtraUsageDisabled"
| "kimiExtraUsageFrozen"
| "kimiExtraUsageUnavailable"
| "kimiMonthlyUsed"
| "kimiMonthlyLimit"
| "kimiMonthlyLimitUnlimited"
| "kimiAdditionalCredits";
type KimiBillingTranslator = (key: KimiBillingTranslationKey, fallback: string) => string;
type KimiBillingCardRow =
| { kind: "balance" | "status"; label: string; value: string }
| {
kind: "link";
label: string;
href: typeof KIMI_CODE_ADDITIONAL_CREDITS_URL;
target: "_blank";
rel: "noreferrer noopener";
};
type JsonRecord = Record<string, unknown>;
function toRecord(value: unknown): JsonRecord | null {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
}
function minorUnits(value: unknown): number | undefined {
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
}
const ISO_4217 = /^[A-Za-z]{3}$/;
const EXTRA_USAGE_STATUSES = new Set<KimiExtraUsageStatus>([
"enabled",
"disabled",
"frozen",
"unavailable",
]);
export function sanitizeKimiBillingStatus(value: unknown): KimiBillingStatus | undefined {
const billing = toRecord(value);
if (!billing || billing.additionalCreditsUrl !== KIMI_CODE_ADDITIONAL_CREDITS_URL)
return undefined;
const currency =
typeof billing.currency === "string" && ISO_4217.test(billing.currency)
? billing.currency.toUpperCase()
: undefined;
const extraUsageStatus =
typeof billing.extraUsageStatus === "string" &&
EXTRA_USAGE_STATUSES.has(billing.extraUsageStatus as KimiExtraUsageStatus)
? (billing.extraUsageStatus as KimiExtraUsageStatus)
: undefined;
if (!currency || !extraUsageStatus) return undefined;
const extraCreditsMinorUnits = minorUnits(billing.extraCreditsMinorUnits);
const monthlyUsedMinorUnits = minorUnits(billing.monthlyUsedMinorUnits);
const monthlyLimitMinorUnits = minorUnits(billing.monthlyLimitMinorUnits);
const monthlyLimitEnabled =
typeof billing.monthlyLimitEnabled === "boolean" ? billing.monthlyLimitEnabled : undefined;
return {
currency,
...(extraCreditsMinorUnits !== undefined ? { extraCreditsMinorUnits } : {}),
...(monthlyUsedMinorUnits !== undefined ? { monthlyUsedMinorUnits } : {}),
...(monthlyLimitEnabled !== undefined ? { monthlyLimitEnabled } : {}),
...(monthlyLimitMinorUnits !== undefined ? { monthlyLimitMinorUnits } : {}),
extraUsageStatus,
additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL,
};
}
function formatKimiMinorUnits(
value: number | undefined,
currency: KimiBillingStatus["currency"],
locales?: Intl.LocalesArgument
): string | null {
if (value === undefined) return null;
return new Intl.NumberFormat(locales, {
style: "currency",
currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value / 100);
}
const fallbackTranslation: KimiBillingTranslator = (_key, fallback) => fallback;
function formatExtraUsageStatus(
status: KimiExtraUsageStatus,
translate: KimiBillingTranslator
): string {
switch (status) {
case "enabled":
return translate("kimiExtraUsageEnabled", "Enabled");
case "disabled":
return translate("kimiExtraUsageDisabled", "Disabled");
case "frozen":
return translate("kimiExtraUsageFrozen", "Frozen");
default:
return translate("kimiExtraUsageUnavailable", "Unavailable");
}
}
export function buildKimiBillingCardRows(
billing: KimiBillingStatus,
locales?: Intl.LocalesArgument,
translate: KimiBillingTranslator = fallbackTranslation
): KimiBillingCardRow[] {
const rows: KimiBillingCardRow[] = [];
const walletPresent = billing.extraCreditsMinorUnits !== undefined;
const extraCredits = formatKimiMinorUnits(
billing.extraCreditsMinorUnits,
billing.currency,
locales
);
if (extraCredits !== null) {
rows.push({
kind: "balance",
label: translate("kimiExtraUsageCredits", "Extra Usage Credits"),
value: extraCredits,
});
}
rows.push({
kind: "status",
label: translate("kimiExtraUsage", "Extra Usage"),
value: formatExtraUsageStatus(billing.extraUsageStatus, translate),
});
if (walletPresent) {
const monthlyUsed = formatKimiMinorUnits(
billing.monthlyUsedMinorUnits,
billing.currency,
locales
);
if (monthlyUsed !== null) {
rows.push({
kind: "status",
label: translate("kimiMonthlyUsed", "Used this month"),
value: monthlyUsed,
});
}
const capped =
billing.monthlyLimitEnabled === true &&
billing.monthlyLimitMinorUnits !== undefined &&
billing.monthlyLimitMinorUnits > 0;
const monthlyLimit = capped
? formatKimiMinorUnits(billing.monthlyLimitMinorUnits, billing.currency, locales)
: null;
rows.push({
kind: "status",
label: translate("kimiMonthlyLimit", "Monthly limit"),
value: monthlyLimit ?? translate("kimiMonthlyLimitUnlimited", "Unlimited"),
});
}
rows.push({
kind: "link",
label: translate("kimiAdditionalCredits", "Additional Credits"),
href: billing.additionalCreditsUrl,
target: "_blank",
rel: "noreferrer noopener",
});
return rows;
}

View File

@@ -0,0 +1,36 @@
import {
GROK_BUILD_ADDITIONAL_CREDITS_URL,
sanitizeGrokBillingStatus,
type GrokBillingStatus,
} from "./grokBilling";
import {
KIMI_CODE_ADDITIONAL_CREDITS_URL,
sanitizeKimiBillingStatus,
type KimiBillingStatus,
} from "./kimiBilling";
export type ProviderBillingStatus = GrokBillingStatus | KimiBillingStatus;
export const PROVIDER_BILLING_PROVIDERS = [
"grok-cli",
"kimi-coding",
"kimi-coding-apikey",
] as const;
export function isProviderBillingProvider(provider: string | undefined): boolean {
return (
provider !== undefined && (PROVIDER_BILLING_PROVIDERS as readonly string[]).includes(provider)
);
}
export function sanitizeProviderBillingStatus(value: unknown): ProviderBillingStatus | undefined {
return sanitizeGrokBillingStatus(value) ?? sanitizeKimiBillingStatus(value);
}
export function isGrokBillingStatus(billing: ProviderBillingStatus): billing is GrokBillingStatus {
return billing.additionalCreditsUrl === GROK_BUILD_ADDITIONAL_CREDITS_URL;
}
export function isKimiBillingStatus(billing: ProviderBillingStatus): billing is KimiBillingStatus {
return billing.additionalCreditsUrl === KIMI_CODE_ADDITIONAL_CREDITS_URL;
}

View File

@@ -0,0 +1,159 @@
import test from "node:test";
import assert from "node:assert/strict";
const { buildKimiBillingCardRows, KIMI_CODE_ADDITIONAL_CREDITS_URL, sanitizeKimiBillingStatus } =
await import("../../src/shared/utils/kimiBilling.ts");
const { isKimiBillingStatus, isProviderBillingProvider, sanitizeProviderBillingStatus } =
await import("../../src/shared/utils/providerBilling.ts");
const { PROVIDER_LABEL } =
await import("../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/constants.ts");
const { USAGE_SUPPORTED_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
const baseBilling = {
currency: "CNY",
extraUsageStatus: "unavailable" as const,
additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL,
};
test("Kimi billing rows show the real Extra Usage status when the wallet is unavailable", () => {
const rows = buildKimiBillingCardRows(baseBilling, "en-US");
assert.deepEqual(rows, [
{ kind: "status", label: "Extra Usage", value: "Unavailable" },
{
kind: "link",
label: "Additional Credits",
href: KIMI_CODE_ADDITIONAL_CREDITS_URL,
target: "_blank",
rel: "noreferrer noopener",
},
]);
});
test("Kimi billing rows show balance, wallet status, monthly spend, cap and buy link", () => {
const rows = buildKimiBillingCardRows(
{
...baseBilling,
extraCreditsMinorUnits: 1234,
monthlyUsedMinorUnits: 250,
monthlyLimitEnabled: true,
monthlyLimitMinorUnits: 5000,
extraUsageStatus: "enabled",
},
"en-US"
);
assert.deepEqual(rows, [
{ kind: "balance", label: "Extra Usage Credits", value: "CN¥12.34" },
{ kind: "status", label: "Extra Usage", value: "Enabled" },
{ kind: "status", label: "Used this month", value: "CN¥2.50" },
{ kind: "status", label: "Monthly limit", value: "CN¥50.00" },
{
kind: "link",
label: "Additional Credits",
href: KIMI_CODE_ADDITIONAL_CREDITS_URL,
target: "_blank",
rel: "noreferrer noopener",
},
]);
});
test("Kimi monthly cap displays Unlimited when disabled or zero", () => {
for (const billing of [
{ ...baseBilling, extraCreditsMinorUnits: 0, monthlyLimitEnabled: false },
{
...baseBilling,
extraCreditsMinorUnits: 0,
monthlyLimitEnabled: true,
monthlyLimitMinorUnits: 0,
},
]) {
const row = buildKimiBillingCardRows(billing, "en-US").find(
(candidate) => candidate.kind === "status" && candidate.label === "Monthly limit"
);
assert.deepEqual(row, { kind: "status", label: "Monthly limit", value: "Unlimited" });
}
});
test("Kimi billing labels support localized translation fallbacks", () => {
const translate = (key: string, fallback: string) =>
({
kimiExtraUsageCredits: "加油包余额",
kimiExtraUsage: "额度加油包",
kimiExtraUsageEnabled: "已开启",
kimiExtraUsageDisabled: "已关闭",
kimiExtraUsageFrozen: "已冻结",
kimiExtraUsageUnavailable: "不可用",
kimiMonthlyUsed: "本月已用",
kimiMonthlyLimit: "每月限额",
kimiMonthlyLimitUnlimited: "无限制",
kimiAdditionalCredits: "充值加油包",
})[key] ?? fallback;
assert.deepEqual(
buildKimiBillingCardRows(
{
...baseBilling,
extraCreditsMinorUnits: 0,
monthlyLimitEnabled: false,
extraUsageStatus: "disabled",
},
"zh-CN",
translate
),
[
{ kind: "balance", label: "加油包余额", value: "¥0.00" },
{ kind: "status", label: "额度加油包", value: "已关闭" },
{ kind: "status", label: "每月限额", value: "无限制" },
{
kind: "link",
label: "充值加油包",
href: KIMI_CODE_ADDITIONAL_CREDITS_URL,
target: "_blank",
rel: "noreferrer noopener",
},
]
);
});
test("Kimi billing sanitizer strips private fields and rejects forged public contracts", () => {
const billing = sanitizeKimiBillingStatus({
currency: "cny",
extraCreditsMinorUnits: 0,
monthlyUsedMinorUnits: 250,
monthlyLimitEnabled: true,
monthlyLimitMinorUnits: 5000,
extraUsageStatus: "disabled",
paymentMethodId: "secret",
additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL,
rawBody: "secret",
});
assert.deepEqual(billing, {
currency: "CNY",
extraCreditsMinorUnits: 0,
monthlyUsedMinorUnits: 250,
monthlyLimitEnabled: true,
monthlyLimitMinorUnits: 5000,
extraUsageStatus: "disabled",
additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL,
});
assert.equal(buildKimiBillingCardRows(billing!, "zh-CN")[0]?.value, "¥0.00");
assert.equal(isKimiBillingStatus(billing!), true);
assert.deepEqual(sanitizeProviderBillingStatus(billing), billing);
for (const forged of [
{ ...baseBilling, currency: "US<script>" },
{ ...baseBilling, extraUsageStatus: "attacker-controlled" },
{ ...baseBilling, additionalCreditsUrl: "https://attacker.invalid/credits" },
]) {
assert.equal(sanitizeKimiBillingStatus(forged), undefined);
}
});
test("Provider Limits registers both Kimi Coding billing providers", () => {
for (const provider of ["kimi-coding", "kimi-coding-apikey"]) {
assert.equal(isProviderBillingProvider(provider), true);
assert.ok((USAGE_SUPPORTED_PROVIDERS as readonly string[]).includes(provider));
}
assert.equal(PROVIDER_LABEL["kimi-coding"], "Kimi Coding");
});

View File

@@ -0,0 +1,419 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-kimi-billing-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.STORAGE_ENCRYPTION_KEY = "kimi-coding-billing-test-key-32-bytes-minimum";
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
const core = await import("../../src/lib/db/core.ts");
const { getUsageForProvider } = await import("../../open-sse/services/usage.ts");
const providerLimitsDb = await import("../../src/lib/db/providerLimits.ts");
const { mergeProviderLimitsCacheEntry, toProviderLimitsCacheEntry } =
await import("../../src/lib/usage/providerLimitsCache.ts");
const { KIMI_CODE_ADDITIONAL_CREDITS_URL } = await import("../../src/shared/utils/kimiBilling.ts");
const originalFetch = globalThis.fetch;
interface KimiUsageResult {
plan?: string;
message?: string;
quotas?: Record<string, unknown>;
billing?: {
currency: string;
extraCreditsMinorUnits?: number;
monthlyUsedMinorUnits?: number;
monthlyLimitEnabled?: boolean;
monthlyLimitMinorUnits?: number;
extraUsageStatus: "enabled" | "disabled" | "frozen" | "unavailable";
additionalCreditsUrl: string;
};
}
function successPayload(boosterWallet?: unknown) {
return {
user: { membership: { level: "LEVEL_ADVANCED" }, email: "must-not-be-exposed@example.invalid" },
usage: {
limit: "1000",
used: "400",
remaining: "600",
resetTime: "2099-08-03T05:20:51Z",
},
limits: [
{
window: { duration: 300, timeUnit: "TIME_UNIT_MINUTE" },
detail: {
limit: "100",
used: "81",
remaining: "19",
resetTime: "2099-08-01T13:38:00Z",
},
},
],
...(boosterWallet === undefined ? {} : { boosterWallet }),
};
}
async function getUsage(
payload: unknown,
connection: {
provider: "kimi-coding" | "kimi-coding-apikey";
accessToken?: string;
apiKey?: string;
} = {
provider: "kimi-coding-apikey",
apiKey: "sk-kimi-fixture",
}
): Promise<KimiUsageResult> {
globalThis.fetch = (async () =>
new Response(JSON.stringify(payload), {
status: 200,
headers: { "content-type": "application/json" },
})) as typeof fetch;
return (await getUsageForProvider({ id: "kimi-connection", ...connection })) as KimiUsageResult;
}
test.afterEach(() => {
globalThis.fetch = originalFetch;
});
test.after(() => {
globalThis.fetch = originalFetch;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("kimi-coding exposes the official boosterWallet Extra Usage contract", async () => {
const calls: Array<{ url: string; headers: Headers }> = [];
globalThis.fetch = (async (input: string | URL | Request, init: RequestInit = {}) => {
calls.push({ url: String(input), headers: new Headers(init.headers) });
return new Response(
JSON.stringify(
successPayload({
balance: {
type: "BOOSTER",
amount: "50000000000",
amountLeft: "1234000000",
},
status: "STATUS_ACTIVE",
monthlyChargeLimitEnabled: true,
monthlyChargeLimit: { priceInCents: "5000", currency: "cny" },
monthlyUsed: { priceInCents: 234, currency: "CNY" },
paymentMethodId: "must-not-be-exposed",
})
),
{ status: 200 }
);
}) as typeof fetch;
const usage = (await getUsageForProvider({
id: "kimi-connection",
provider: "kimi-coding-apikey",
apiKey: "sk-kimi-fixture",
})) as KimiUsageResult;
assert.equal(usage.plan, "Allegro");
assert.deepEqual(usage.quotas, {
code_7d: {
displayName: "Code · 7d",
used: 400,
total: 1000,
remaining: 600,
remainingPercentage: 60,
resetAt: "2099-08-03T05:20:51.000Z",
unlimited: false,
},
code_5h: {
displayName: "Code · 5h",
used: 81,
total: 100,
remaining: 19,
remainingPercentage: 19,
resetAt: "2099-08-01T13:38:00.000Z",
unlimited: false,
},
});
assert.deepEqual(usage.billing, {
currency: "CNY",
extraCreditsMinorUnits: 1234,
monthlyUsedMinorUnits: 234,
monthlyLimitEnabled: true,
monthlyLimitMinorUnits: 5000,
extraUsageStatus: "enabled",
additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL,
});
assert.equal(calls.length, 1, "billing must come from the existing /usages read");
assert.equal(calls[0]?.url, "https://api.kimi.com/coding/v1/usages");
assert.equal(calls[0]?.headers.get("x-api-key"), "sk-kimi-fixture");
assert.equal(calls[0]?.headers.get("authorization"), null);
const serialized = JSON.stringify(usage);
assert.equal(serialized.includes("sk-kimi-fixture"), false);
assert.equal(serialized.includes("must-not-be-exposed"), false);
assert.equal(serialized.includes("paymentMethodId"), false);
});
test("booster fixed-point values follow the official Kimi CLI cent conversion", async () => {
for (const [amountLeft, expected] of [
[0, 0],
[1, 1],
[999_999, 1],
[1_000_000, 1],
[1_499_999, 1],
[1_500_000, 2],
[1_234_000_000, 1234],
] as const) {
const usage = await getUsage(
successPayload({
balance: { type: "BOOSTER", amount: 50_000_000, amountLeft },
monthlyChargeLimitEnabled: false,
})
);
assert.equal(usage.billing?.extraCreditsMinorUnits, expected);
}
});
test("missing and invalid booster wallets do not fabricate a balance", async () => {
for (const boosterWallet of [
undefined,
{},
{ balance: { type: "OTHER", amount: 50_000_000, amountLeft: 25_000_000 } },
{ balance: { type: "BOOSTER" }, status: "STATUS_UNKNOWN" },
{ balance: { type: "BOOSTER" }, allowTopup: false },
{ balance: { type: "BOOSTER" }, allowTopup: true },
]) {
const usage = await getUsage(successPayload(boosterWallet));
assert.deepEqual(usage.billing, {
currency: "USD",
extraUsageStatus: "unavailable",
additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL,
});
}
const productionDisabled = await getUsage(
successPayload({
status: "STATUS_DISABLED",
allowTopup: true,
balance: { type: "BOOSTER", unit: "UNIT_CURRENCY" },
monthlyChargeLimit: { priceInCents: 10000, currency: "CNY" },
monthlyUsed: { priceInCents: 0, currency: "CNY" },
})
);
assert.deepEqual(productionDisabled.billing, {
currency: "CNY",
extraCreditsMinorUnits: 0,
monthlyUsedMinorUnits: 0,
monthlyLimitEnabled: false,
monthlyLimitMinorUnits: 10000,
extraUsageStatus: "disabled",
additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL,
});
});
test("count quotas derive missing used or remaining and preserve every window", async () => {
const usage = await getUsage({
user: { membership: { level: "LEVEL_ADVANCED" } },
usage: { limit: "100", remaining: "84", resetTime: "2099-08-26T00:38:00Z" },
limits: [
{
window: { duration: 300, timeUnit: "TIME_UNIT_MINUTE" },
detail: { limit: "100", used: "81", resetTime: "2099-08-19T05:38:00Z" },
},
{
window: { duration: 7, timeUnit: "TIME_UNIT_DAY" },
detail: { limit: "200", used: "40", remaining: "160" },
},
{
window: { duration: 300, timeUnit: "TIME_UNIT_MINUTE" },
detail: { limit: "50", remaining: "25" },
},
],
});
assert.deepEqual(usage.quotas, {
code_7d: {
displayName: "Code · 7d",
used: 16,
total: 100,
remaining: 84,
remainingPercentage: 84,
resetAt: "2099-08-26T00:38:00.000Z",
unlimited: false,
},
code_5h: {
displayName: "Code · 5h",
used: 81,
total: 100,
remaining: 19,
remainingPercentage: 19,
resetAt: "2099-08-19T05:38:00.000Z",
unlimited: false,
},
code_7d_2: {
displayName: "Code · 7d",
used: 40,
total: 200,
remaining: 160,
remainingPercentage: 80,
resetAt: null,
unlimited: false,
},
code_5h_2: {
displayName: "Code · 5h",
used: 25,
total: 50,
remaining: 25,
remainingPercentage: 50,
resetAt: null,
unlimited: false,
},
});
});
test("currency precedence and monthly-cap fields match the official parser", async () => {
const fromLimit = await getUsage(
successPayload({
balance: { type: "BOOSTER", amount: 50_000_000, amountLeft: 10_000_000 },
monthlyChargeLimitEnabled: true,
monthlyChargeLimit: { priceInCents: 5000, currency: "CNY" },
monthlyUsed: { priceInCents: 10, currency: "USD" },
})
);
assert.equal(fromLimit.billing?.currency, "CNY");
const fromUsed = await getUsage(
successPayload({
balance: { type: "BOOSTER", amount: 50_000_000, amountLeft: 10_000_000 },
monthlyChargeLimitEnabled: false,
monthlyUsed: { priceInCents: "0", currency: "cny" },
})
);
assert.deepEqual(fromUsed.billing, {
currency: "CNY",
extraCreditsMinorUnits: 10,
monthlyUsedMinorUnits: 0,
monthlyLimitEnabled: false,
monthlyLimitMinorUnits: 0,
extraUsageStatus: "unavailable",
additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL,
});
const fallback = await getUsage(
successPayload({
balance: { type: "BOOSTER", amount: 50_000_000, amountLeft: 10_000_000 },
})
);
assert.equal(fallback.billing?.currency, "USD");
});
test("both Kimi auth modes receive billing without additional requests", async () => {
const calls: Array<{ headers: Headers }> = [];
globalThis.fetch = (async (_input: string | URL | Request, init: RequestInit = {}) => {
calls.push({ headers: new Headers(init.headers) });
return new Response(JSON.stringify(successPayload()), { status: 200 });
}) as typeof fetch;
await getUsageForProvider({
id: "kimi-api-key",
provider: "kimi-coding-apikey",
apiKey: "sk-kimi-key",
});
await getUsageForProvider({
id: "kimi-oauth",
provider: "kimi-coding",
accessToken: "oauth-token",
providerSpecificData: {
deviceId: "123456781234123412341234567890ab",
deviceName: "host",
deviceModel: "model",
osVersion: "os",
},
});
assert.equal(calls.length, 2);
assert.equal(calls[0]?.headers.get("x-api-key"), "sk-kimi-key");
assert.equal(calls[1]?.headers.get("authorization"), "Bearer oauth-token");
assert.equal(calls[1]?.headers.get("x-api-key"), null);
});
test("Kimi usage failures do not expose or synthesize billing", async () => {
const sensitive = "secret-upstream-body";
globalThis.fetch = (async () => new Response(sensitive, { status: 500 })) as typeof fetch;
const usage = (await getUsageForProvider({
id: "kimi-connection",
provider: "kimi-coding-apikey",
apiKey: "sk-kimi-secret",
})) as KimiUsageResult;
assert.equal(usage.billing, undefined);
assert.ok(usage.message);
assert.equal(JSON.stringify(usage).includes("sk-kimi-secret"), false);
});
test("Provider Limits sanitizes and persists only the public Kimi billing contract", () => {
const rawBilling = {
currency: "cny",
extraCreditsMinorUnits: 0,
monthlyUsedMinorUnits: 125,
monthlyLimitEnabled: true,
monthlyLimitMinorUnits: 5000,
extraUsageStatus: "disabled",
paymentMethodId: "secret",
additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL,
rawBody: "secret",
};
const cacheEntry = toProviderLimitsCacheEntry(
{ quotas: { code_7d: { remainingPercentage: 60 } }, billing: rawBilling },
"manual",
"2026-08-19T00:00:00.000Z"
);
assert.deepEqual(cacheEntry.billing, {
currency: "CNY",
extraCreditsMinorUnits: 0,
monthlyUsedMinorUnits: 125,
monthlyLimitEnabled: true,
monthlyLimitMinorUnits: 5000,
extraUsageStatus: "disabled",
additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL,
});
assert.equal(JSON.stringify(cacheEntry).includes("secret"), false);
const stored = providerLimitsDb.setProviderLimitsCache("kimi-connection", {
...cacheEntry,
billing: rawBilling as unknown as NonNullable<typeof cacheEntry.billing>,
});
assert.deepEqual(stored.billing, cacheEntry.billing);
assert.deepEqual(
providerLimitsDb.getProviderLimitsCache("kimi-connection")?.billing,
stored.billing
);
assert.equal(JSON.stringify(stored).includes("secret"), false);
});
test("message-only Kimi failures preserve last-known-good billing", () => {
const previous = {
quotas: null,
plan: "Allegro",
message: null,
fetchedAt: "2026-08-18T00:00:00.000Z",
billing: {
currency: "CNY",
extraCreditsMinorUnits: 2500,
extraUsageStatus: "unavailable" as const,
additionalCreditsUrl: KIMI_CODE_ADDITIONAL_CREDITS_URL,
},
};
const failure = {
quotas: null,
plan: "Kimi Coding",
message: "Kimi Coding connected. API Error 500",
fetchedAt: "2026-08-19T00:00:00.000Z",
};
assert.equal(mergeProviderLimitsCacheEntry("kimi-coding", failure, previous), previous);
assert.equal(mergeProviderLimitsCacheEntry("kimi-coding-apikey", failure, previous), previous);
});

View File

@@ -60,6 +60,59 @@ test("#6687 glm family: display order stays session-then-weekly regardless of re
);
});
test("Kimi Coding: display order stays Code 5h then Code 7d regardless of remaining %", () => {
const rawKimiData = {
quotas: {
code_7d: {
displayName: "Code · 7d",
used: 10,
total: 100,
remainingPercentage: 90,
resetAt: null,
},
code_5h: {
displayName: "Code · 5h",
used: 95,
total: 100,
remainingPercentage: 5,
resetAt: null,
},
code_7d_2: {
displayName: "Code · 7d",
used: 20,
total: 100,
remainingPercentage: 80,
resetAt: null,
},
},
};
const parsed = parseQuotaData("kimi-coding", rawKimiData);
assert.deepEqual(
parsed.map((q: TestQuota) => q.name),
["code_5h", "code_7d", "code_7d_2"]
);
assert.deepEqual(
resolveQuotaDisplayOrder("kimi-coding", parsed).map((q: TestQuota) => q.name),
["code_5h", "code_7d", "code_7d_2"]
);
});
test("Kimi Coding API-key provider uses the same fixed window order", () => {
const parsed = parseQuotaData("kimi-coding-apikey", {
quotas: {
code_7d: { remainingPercentage: 90 },
code_5h_2: { remainingPercentage: 80 },
code_5h: { remainingPercentage: 5 },
code_7d_2: { remainingPercentage: 95 },
},
});
assert.deepEqual(
parsed.map((q: TestQuota) => q.name),
["code_5h", "code_5h_2", "code_7d", "code_7d_2"]
);
});
test("#6687 non-fixed-order providers still sort by remaining percentage descending", () => {
const quotas = [
{ name: "low", remainingPercentage: 10 },

View File

@@ -35,6 +35,24 @@ test("#7764: topQuotas() (collapsed card order) respects hasFixedQuotaOrder inst
assert.deepEqual(renderedB, ["session", "weekly"]);
});
test("Kimi Coding collapsed quota order stays Code 5h then Code 7d across refreshes", () => {
const parsedA = parseQuotaData("kimi-coding", {
quotas: {
code_7d: { used: 10, total: 100, remainingPercentage: 90 },
code_5h: { used: 95, total: 100, remainingPercentage: 5 },
},
});
const parsedB = parseQuotaData("kimi-coding", {
quotas: {
code_7d: { used: 99, total: 100, remainingPercentage: 1 },
code_5h: { used: 20, total: 100, remainingPercentage: 80 },
},
});
assert.deepEqual(topQuotas(parsedA, 3, "kimi-coding").map(quotaName), ["code_5h", "code_7d"]);
assert.deepEqual(topQuotas(parsedB, 3, "kimi-coding").map(quotaName), ["code_5h", "code_7d"]);
});
test("#7764: providers WITHOUT a fixed order still sort worst-status-first (no regression)", () => {
const quotas = [
{ name: "alpha", used: 10, total: 100, remainingPercentage: 90 },

View File

@@ -706,6 +706,7 @@ test("usage service covers Codex, Kiro and Kimi usage parsing and error branches
},
limits: [
{
window: { duration: 300, timeUnit: "TIME_UNIT_MINUTE" },
detail: {
limit: "20",
remaining: "3",
@@ -771,8 +772,8 @@ test("usage service covers Codex, Kiro and Kimi usage parsing and error branches
accessToken: "kimi-token",
});
assert.equal(kimi.plan, "Allegro");
assert.equal(kimi.quotas.Weekly.remaining, 8);
assert.equal(kimi.quotas.Ratelimit.remaining, 3);
assert.equal(kimi.quotas.code_7d.remaining, 8);
assert.equal(kimi.quotas.code_5h.remaining, 3);
assert.equal(kimi.quotas["session (5h)"].remaining, 25);
globalThis.fetch = async (url) => {