mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 14:12:59 +03:00
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:
1
changelog.d/features/kimi-coding-extra-usage.md
Normal file
1
changelog.d/features/kimi-coding-extra-usage.md
Normal 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.
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -9077,6 +9077,16 @@
|
||||
"grokAutoTopUpMax": "最大",
|
||||
"grokAutoTopUpMonth": "月",
|
||||
"grokAdditionalCredits": "额外的致谢",
|
||||
"kimiExtraUsageCredits": "加油包余额",
|
||||
"kimiExtraUsage": "额度加油包",
|
||||
"kimiExtraUsageEnabled": "已开启",
|
||||
"kimiExtraUsageDisabled": "已关闭",
|
||||
"kimiExtraUsageFrozen": "已冻结",
|
||||
"kimiExtraUsageUnavailable": "不可用",
|
||||
"kimiMonthlyUsed": "本月已用",
|
||||
"kimiMonthlyLimit": "每月限额",
|
||||
"kimiMonthlyLimitUnlimited": "无限制",
|
||||
"kimiAdditionalCredits": "充值加油包",
|
||||
"loggerTab": "记录器",
|
||||
"proxyTab": "代理",
|
||||
"budgetManagement": "预算管理",
|
||||
|
||||
@@ -9077,6 +9077,16 @@
|
||||
"grokAutoTopUpMax": "最大",
|
||||
"grokAutoTopUpMonth": "月份",
|
||||
"grokAdditionalCredits": "額外的致謝",
|
||||
"kimiExtraUsageCredits": "加油包餘額",
|
||||
"kimiExtraUsage": "額度加油包",
|
||||
"kimiExtraUsageEnabled": "已開啟",
|
||||
"kimiExtraUsageDisabled": "已關閉",
|
||||
"kimiExtraUsageFrozen": "已凍結",
|
||||
"kimiExtraUsageUnavailable": "無法使用",
|
||||
"kimiMonthlyUsed": "本月已用",
|
||||
"kimiMonthlyLimit": "每月限額",
|
||||
"kimiMonthlyLimitUnlimited": "無限制",
|
||||
"kimiAdditionalCredits": "儲值加油包",
|
||||
"loggerTab": "記錄器",
|
||||
"proxyTab": "代理",
|
||||
"budgetManagement": "預算管理",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
199
src/shared/utils/kimiBilling.ts
Normal file
199
src/shared/utils/kimiBilling.ts
Normal 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;
|
||||
}
|
||||
36
src/shared/utils/providerBilling.ts
Normal file
36
src/shared/utils/providerBilling.ts
Normal 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;
|
||||
}
|
||||
159
tests/unit/kimi-coding-billing-ui.test.ts
Normal file
159
tests/unit/kimi-coding-billing-ui.test.ts
Normal 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");
|
||||
});
|
||||
419
tests/unit/kimi-coding-billing.test.ts
Normal file
419
tests/unit/kimi-coding-billing.test.ts
Normal 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);
|
||||
});
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user