mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 06:12:17 +03:00
feat(usage): add Kilo Code balance and Kilo Pass quotas (#12178)
* feat(usage): add Kilo Code balance and Kilo Pass quotas * feat(usage): add Kilo Pass dashboard meter * test(usage): cover Kilo Code quota integration * docs(usage): document Kilo API endpoint override
This commit is contained in:
@@ -2892,6 +2892,14 @@ QUOTA_STORE_DRIVER=sqlite
|
||||
# PROMPTQL_TOKEN_REFRESH_URL=https://auth.pro.ql.app/ddn/project/token
|
||||
# PROMPTQL_POLL_TIMEOUT_MS=180000
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Kilo Code usage quotas (src/shared/constants/providers/kilocode.ts)
|
||||
# Personal USD balance and Kilo Pass usage lookup. Optional — the default
|
||||
# points at the public Kilo API; override only for a relay/test fixture.
|
||||
# Authentication uses the connection's existing OAuth access token.
|
||||
# Used by: open-sse/services/usage/kilocode.ts
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# KILO_API_URL=https://api.kilo.ai
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# HyperAgent web provider (Unofficial/Experimental — src/shared/constants/providers/web-cookie.ts)
|
||||
# Reverse-engineered session bridge for hyperagent.com. Optional — defaults
|
||||
|
||||
@@ -975,6 +975,16 @@ Reverse-engineered session bridge for hyperagent.com (`src/shared/constants/prov
|
||||
|
||||
---
|
||||
|
||||
## Kilo Code Usage Quotas
|
||||
|
||||
Personal USD balance and Kilo Pass usage lookup for the Kilo Code provider. Optional — the default points at the public Kilo API; override only for a relay/test fixture. Authentication uses the connection's existing OAuth access token.
|
||||
|
||||
| Variable | Default | Source File | Description |
|
||||
| ---------------- | ---------------------- | ----------------------------------------- | ------------------------------------------------------- |
|
||||
| `KILO_API_URL` | `https://api.kilo.ai` | `open-sse/services/usage/kilocode.ts` | Base URL used to fetch personal Kilo Code balance and Kilo Pass usage. |
|
||||
|
||||
---
|
||||
|
||||
## Adobe Firefly Web Provider (Unofficial/Experimental)
|
||||
|
||||
Browser-driven session refresh for the Adobe Firefly web provider
|
||||
|
||||
@@ -73,6 +73,7 @@ import { getCommandCodeUsage } from "./usage/command-code.ts";
|
||||
import { getQwenTokenPlanUsage } from "./usage/qwen-token-plan.ts";
|
||||
import { getConolUsage } from "./conolUsage.ts";
|
||||
import { getAgentrouterUsage } from "./usage/agentrouter.ts";
|
||||
import { getKilocodeUsage } from "./usage/kilocode.ts";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
type UsageProviderConnection = JsonRecord & {
|
||||
@@ -205,6 +206,8 @@ export async function getUsageForProvider(
|
||||
return await getConolUsage(apiKey || accessToken, providerSpecificData);
|
||||
case "agentrouter":
|
||||
return await getAgentrouterUsage(id, connection);
|
||||
case "kilocode":
|
||||
return await getKilocodeUsage(id, connection);
|
||||
default:
|
||||
return { message: `Usage API not implemented for ${provider}` };
|
||||
}
|
||||
@@ -244,4 +247,5 @@ export const __testing = {
|
||||
mapSubscriptionTierStringToPlanLabel,
|
||||
toDisplayLabel,
|
||||
getKiroUsage,
|
||||
getKilocodeUsage,
|
||||
};
|
||||
|
||||
@@ -73,6 +73,7 @@ export const USAGE_FETCHER_PROVIDERS = [
|
||||
"cnl",
|
||||
// AgentRouter (New-API) console balance (GET /api/user/self)
|
||||
"agentrouter",
|
||||
"kilocode",
|
||||
] as const;
|
||||
|
||||
export type UsageFetcherProvider = (typeof USAGE_FETCHER_PROVIDERS)[number];
|
||||
|
||||
334
open-sse/services/usage/kilocode.ts
Normal file
334
open-sse/services/usage/kilocode.ts
Normal file
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* usage/kilocode.ts — Kilo Code balance + Kilo Pass usage fetcher (Provider Limits).
|
||||
*
|
||||
* Two independent upstream requests per usage fetch, both authenticated with the
|
||||
* existing kilocode OAuth access token (personal scope; no organization support):
|
||||
* - GET {KILO_API_URL|https://api.kilo.ai}/api/profile/balance → personal USD balance
|
||||
* - GET {KILO_API_URL|https://api.kilo.ai}/api/trpc/kiloPass.getState?batch=1&input={"0":null}
|
||||
* → Kilo Pass subscription state (official tRPC endpoint, Kilo-Org/kilocode contract)
|
||||
*
|
||||
* The two requests fail independently: a Kilo Pass error never hides the personal
|
||||
* balance and vice versa. Only when both are unavailable does the dashboard fall
|
||||
* back to the existing { message } convention.
|
||||
*/
|
||||
import type { UsageQuota } from "./quota.ts";
|
||||
import { parseResetTime } from "./quota.ts";
|
||||
import { toRecord, toNumber, roundCurrency } from "./scalars.ts";
|
||||
|
||||
/** Upstream API base. Environment override mirrors sibling fetchers. */
|
||||
const KILO_API_BASE: string = process.env.KILO_API_URL || "https://api.kilo.ai";
|
||||
const BALANCE_PATH = "/api/profile/balance";
|
||||
const BALANCE_URL = `${KILO_API_BASE}${BALANCE_PATH}`;
|
||||
const PASS_PATH = "/api/trpc/kiloPass.getState";
|
||||
|
||||
const KILO_EDITOR_NAME = "OmniRoute";
|
||||
const FETCH_TIMEOUT_MS = 8_000;
|
||||
|
||||
/** Fallback token for Kilo's anonymous freetier (registry anonymousApiKey).
|
||||
* Balance/pass endpoints require authenticated accounts, value rejected
|
||||
* before any request made. */
|
||||
const KILO_ANONYMOUS_TOKEN = "anonymous";
|
||||
|
||||
/** Live subscription statuses that represent an active Kilo Pass, per the
|
||||
* official Kilo-Org/kilocode parseKiloPassState contract. The cloud returns
|
||||
* full records after cancellation too; only these statuses consume credits. */
|
||||
const KILO_PASS_LIVE_STATUSES = new Set(["active", "past_due", "trialing"]);
|
||||
|
||||
/** Kilo Pass subscription state (mirrors official Kilo-Org/kilocode KiloPassState). */
|
||||
export interface KiloPassState {
|
||||
currentPeriodBaseCreditsUsd: number;
|
||||
currentPeriodUsageUsd: number;
|
||||
currentPeriodBonusCreditsUsd: number;
|
||||
nextBillingAt: string | null;
|
||||
}
|
||||
|
||||
function readAccessToken(connection: Record<string, unknown>): string | null {
|
||||
const value = connection["accessToken"];
|
||||
if (typeof value === "string" && value.trim().length > 0) return value;
|
||||
return null;
|
||||
}
|
||||
|
||||
function isAnonymousToken(token: string): boolean {
|
||||
return token.trim() === KILO_ANONYMOUS_TOKEN;
|
||||
}
|
||||
|
||||
function kiloHeaders(token: string): Record<string, string> {
|
||||
return {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"X-KILOCODE-EDITORNAME": KILO_EDITOR_NAME,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
};
|
||||
}
|
||||
|
||||
/** Extract non-negative USD balance from upstream JSON body. Returns null
|
||||
* when value missing, null, negative, not numeric. */
|
||||
export function parseKilocodeBalance(data: unknown): number | null {
|
||||
const obj = toRecord(data);
|
||||
if (obj.balance === undefined || obj.balance === null) return null;
|
||||
const balance = toNumber(obj.balance, Number.NaN);
|
||||
if (!Number.isFinite(balance) || balance < 0) return null;
|
||||
return roundCurrency(balance);
|
||||
}
|
||||
|
||||
/** Coerce a USD credit amount the way the official client does: finite
|
||||
* non-negative numbers pass through, everything else becomes 0. */
|
||||
function passUsd(value: unknown): number {
|
||||
const num = toNumber(value, 0);
|
||||
return Number.isFinite(num) && num >= 0 ? num : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Kilo Pass state from the tRPC response, mirroring the official
|
||||
* Kilo-Org/kilocode parseKiloPassState semantics exactly:
|
||||
* - batched tRPC shape: [{ result: { data: { json: { subscription } } } }]
|
||||
* - unbatched result.data.json: { result: { data: { json: { subscription } } } }
|
||||
* - plain result.data (no superjson json wrapper): { result: { data: { subscription } } }
|
||||
* - plain fallback: { subscription }
|
||||
* - requires at least one period amount present (base or usage)
|
||||
* - status, when present as string, must be a live status
|
||||
* - negative/non-finite amounts clamp to 0; invalid dates become null
|
||||
*
|
||||
* Returns null when no live pass data is present (no pass, canceled, expired,
|
||||
* missing fields, malformed tRPC envelope).
|
||||
*/
|
||||
export function parseKiloPassState(value: unknown): KiloPassState | null {
|
||||
const item = Array.isArray(value) ? value[0] : value;
|
||||
const data = toRecord(toRecord(toRecord(item)?.result)?.data);
|
||||
// Official Kilo-Org/kilocode fallback chain: data.json envelope first,
|
||||
// then the tRPC data object itself (plain-JSON responses carry the
|
||||
// subscription there without a superjson json wrapper), then raw payload.
|
||||
const jsonValue = data?.json;
|
||||
const root =
|
||||
jsonValue !== null && typeof jsonValue === "object" && !Array.isArray(jsonValue)
|
||||
? toRecord(jsonValue)
|
||||
: Object.keys(data).length > 0
|
||||
? data
|
||||
: toRecord(value);
|
||||
const sub = toRecord(root?.subscription);
|
||||
|
||||
if (!sub || (sub.currentPeriodBaseCreditsUsd == null && sub.currentPeriodUsageUsd == null)) {
|
||||
return null;
|
||||
}
|
||||
if (typeof sub.status === "string" && !KILO_PASS_LIVE_STATUSES.has(sub.status)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const next = sub.nextBillingAt ?? sub.nextRenewalAt;
|
||||
|
||||
return {
|
||||
currentPeriodBaseCreditsUsd: passUsd(sub.currentPeriodBaseCreditsUsd),
|
||||
currentPeriodUsageUsd: passUsd(sub.currentPeriodUsageUsd),
|
||||
currentPeriodBonusCreditsUsd: passUsd(sub.currentPeriodBonusCreditsUsd),
|
||||
// Normalize to the ISO format OmniRoute expects; invalid dates must not
|
||||
// break the whole fetch (parseResetTime returns null instead).
|
||||
nextBillingAt: parseResetTime(typeof next === "string" ? next : null),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Kilo Pass state. Returns null on any failure (HTTP error, network,
|
||||
* timeout, malformed body, no live pass) — matches the official client's
|
||||
* silent-degradation contract. Never throws, never logs tokens/bodies.
|
||||
*/
|
||||
export async function fetchKiloPassState(token: string): Promise<KiloPassState | null> {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
batch: "1",
|
||||
input: JSON.stringify({ "0": null }),
|
||||
});
|
||||
const response = await fetch(`${KILO_API_BASE}${PASS_PATH}?${params}`, {
|
||||
method: "GET",
|
||||
headers: kiloHeaders(token),
|
||||
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
return parseKiloPassState(await response.json());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Build normalized usage response from successful balance fetch. */
|
||||
export function buildKilocodeUsageResult(balance: number): {
|
||||
plan: string;
|
||||
quotas: Record<string, UsageQuota>;
|
||||
} {
|
||||
const balanceQuota: UsageQuota = {
|
||||
used: 0,
|
||||
total: 0,
|
||||
remaining: balance,
|
||||
remainingPercentage: balance > 0 ? 100 : 0,
|
||||
resetAt: null,
|
||||
unlimited: true,
|
||||
currency: "USD",
|
||||
displayName: "Balance (USD)",
|
||||
};
|
||||
|
||||
return {
|
||||
plan: "Kilo Code",
|
||||
quotas: { balance: balanceQuota },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Kilo Pass quota entries. Remaining pass credits follow the official
|
||||
* Kilo Pass meter semantics (total pool = base + bonus, used consumed from it):
|
||||
* remaining = max(0, base + bonus - usage). resetAt carries nextBillingAt on
|
||||
* the period-defining Base Credits row.
|
||||
*/
|
||||
export function buildKiloPassUsageResult(pass: KiloPassState): {
|
||||
plan: string;
|
||||
quotas: Record<string, UsageQuota>;
|
||||
} {
|
||||
const base = pass.currentPeriodBaseCreditsUsd;
|
||||
const bonus = pass.currentPeriodBonusCreditsUsd;
|
||||
const usage = pass.currentPeriodUsageUsd;
|
||||
const remaining = Math.max(0, roundCurrency(base + bonus - usage));
|
||||
|
||||
const quotas: Record<string, UsageQuota> = {
|
||||
kiloPassBase: {
|
||||
used: 0,
|
||||
total: base,
|
||||
remaining: base,
|
||||
remainingPercentage: base > 0 ? 100 : 0,
|
||||
resetAt: pass.nextBillingAt,
|
||||
unlimited: false,
|
||||
currency: "USD",
|
||||
displayName: "Base Credits",
|
||||
},
|
||||
kiloPassBonus: {
|
||||
used: 0,
|
||||
total: bonus,
|
||||
remaining: bonus,
|
||||
remainingPercentage: bonus > 0 ? 100 : 0,
|
||||
resetAt: null,
|
||||
unlimited: false,
|
||||
currency: "USD",
|
||||
displayName: "Bonus Credits",
|
||||
},
|
||||
kiloPassUsage: {
|
||||
used: usage,
|
||||
total: base + bonus,
|
||||
remaining,
|
||||
remainingPercentage: base + bonus > 0 ? Math.max(0, (remaining / (base + bonus)) * 100) : 0,
|
||||
resetAt: pass.nextBillingAt,
|
||||
unlimited: false,
|
||||
currency: "USD",
|
||||
displayName: "Kilo Pass Usage",
|
||||
},
|
||||
kiloPassRemaining: {
|
||||
used: 0,
|
||||
total: 0,
|
||||
remaining,
|
||||
remainingPercentage: remaining > 0 ? 100 : 0,
|
||||
resetAt: pass.nextBillingAt,
|
||||
unlimited: false,
|
||||
currency: "USD",
|
||||
displayName: "Pass Remaining",
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
plan: "Kilo Code",
|
||||
quotas,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch balance from upstream API. Throws with the historical per-status
|
||||
* messages so getKilocodeUsage can surface the same diagnostics as before
|
||||
* when the pass request fails alongside it.
|
||||
*/
|
||||
async function fetchBalance(token: string): Promise<number> {
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(BALANCE_URL, {
|
||||
method: "GET",
|
||||
headers: kiloHeaders(token),
|
||||
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Kilo Code balance error: ${(error as Error).message}`);
|
||||
}
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new Error("Kilo Code token expired access denied. Please re-authenticate connection.");
|
||||
}
|
||||
if (response.status === 429) {
|
||||
throw new Error("Kilo Code balance request rate limited. Try again later.");
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`Kilo Code balance request failed with HTTP ${response.status}.`);
|
||||
}
|
||||
|
||||
let data: unknown;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch (error) {
|
||||
throw new Error(`Kilo Code balance error: ${(error as Error).message}`);
|
||||
}
|
||||
|
||||
const balance = parseKilocodeBalance(data);
|
||||
if (balance === null) {
|
||||
throw new Error("Kilo Code balance response invalid missing balance value.");
|
||||
}
|
||||
return balance;
|
||||
}
|
||||
|
||||
/** Fetch and normalize Kilo Code balance + Kilo Pass usage for connection. */
|
||||
export async function getKilocodeUsage(
|
||||
_connectionId: string | undefined,
|
||||
connection?: Record<string, unknown>
|
||||
): Promise<
|
||||
{ plan: string; quotas: Record<string, UsageQuota> } | { plan: string; message: string }
|
||||
> {
|
||||
const token = connection ? readAccessToken(connection) : null;
|
||||
if (connection?.["apiKey"] !== undefined && !token) {
|
||||
return {
|
||||
plan: "Kilo Code",
|
||||
message: "Kilo Code balance uses Kilo Code OAuth account; separate API key not supported.",
|
||||
};
|
||||
}
|
||||
if (!token) {
|
||||
return {
|
||||
plan: "Kilo Code",
|
||||
message: "Kilo Code balance not available. Add Kilo Code account view usage.",
|
||||
};
|
||||
}
|
||||
if (isAnonymousToken(token)) {
|
||||
return {
|
||||
plan: "Kilo Code",
|
||||
message:
|
||||
"Kilo Code balance only available authenticated accounts. Free anonymous usage balance.",
|
||||
};
|
||||
}
|
||||
|
||||
// Both requests share one usage fetch but fail independently.
|
||||
const [balanceSettled, passSettled] = await Promise.allSettled([
|
||||
fetchBalance(token),
|
||||
fetchKiloPassState(token),
|
||||
]);
|
||||
|
||||
const balance = balanceSettled.status === "fulfilled" ? balanceSettled.value : null;
|
||||
const pass = passSettled.status === "fulfilled" ? passSettled.value : null;
|
||||
|
||||
if (balance !== null && pass !== null) {
|
||||
return {
|
||||
plan: "Kilo Code",
|
||||
quotas: {
|
||||
...buildKilocodeUsageResult(balance).quotas,
|
||||
...buildKiloPassUsageResult(pass).quotas,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (balance !== null) return buildKilocodeUsageResult(balance);
|
||||
if (pass !== null) return buildKiloPassUsageResult(pass);
|
||||
|
||||
const balanceError =
|
||||
balanceSettled.status === "rejected" ? (balanceSettled.reason as Error).message : null;
|
||||
return {
|
||||
plan: "Kilo Code",
|
||||
message: balanceError ?? "Kilo Code usage unavailable. Try again later.",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
import { getBarColor } from "../utils";
|
||||
import { translateUsageOrFallback } from "../i18nFallback";
|
||||
|
||||
interface KiloPassMeterProps {
|
||||
base: number;
|
||||
bonus: number;
|
||||
used: number;
|
||||
total: number;
|
||||
remaining: number;
|
||||
nextBillingAt?: string | null;
|
||||
balance?: number | null;
|
||||
}
|
||||
|
||||
function formatCurrency(value: number, currency: string = "USD"): string {
|
||||
return value.toLocaleString(undefined, {
|
||||
style: "currency",
|
||||
currency,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
|
||||
function calculateDaysUntil(dateString: string | null | undefined): number | null {
|
||||
if (!dateString) return null;
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
if (Number.isNaN(date.getTime())) return null;
|
||||
const now = Date.now();
|
||||
const diff = date.getTime() - now;
|
||||
if (diff <= 0) return null;
|
||||
return Math.ceil(diff / (1000 * 60 * 60 * 24));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
type KiloPassMeterValues = Pick<
|
||||
KiloPassMeterProps,
|
||||
"base" | "bonus" | "used" | "total" | "remaining"
|
||||
>;
|
||||
|
||||
function nonNegativeNumber(value: number): number {
|
||||
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
||||
}
|
||||
|
||||
function percentage(value: number, total: number): number {
|
||||
return total > 0 ? Math.min(100, (value / total) * 100) : 0;
|
||||
}
|
||||
|
||||
export function buildKiloPassMeterModel({
|
||||
base,
|
||||
bonus,
|
||||
used,
|
||||
total,
|
||||
remaining,
|
||||
}: KiloPassMeterValues) {
|
||||
const paid = nonNegativeNumber(base);
|
||||
const bonusAmount = nonNegativeNumber(bonus);
|
||||
const usedAmount = nonNegativeNumber(used);
|
||||
const totalAmount = nonNegativeNumber(total);
|
||||
const remainingAmount = nonNegativeNumber(remaining);
|
||||
const paidUsed = Math.min(usedAmount, paid);
|
||||
const bonusUsed = Math.min(Math.max(usedAmount - paid, 0), bonusAmount);
|
||||
|
||||
return {
|
||||
paid,
|
||||
bonus: bonusAmount,
|
||||
used: usedAmount,
|
||||
total: totalAmount,
|
||||
remaining: remainingAmount,
|
||||
progressValue: Math.min(totalAmount, usedAmount),
|
||||
paidPercent: percentage(paid, totalAmount),
|
||||
bonusPercent: percentage(bonusAmount, totalAmount),
|
||||
paidUsedPercent: percentage(paidUsed, paid),
|
||||
bonusUsedPercent: percentage(bonusUsed, bonusAmount),
|
||||
hasPaidSegment: paid > 0,
|
||||
hasBonusSegment: bonusAmount > 0,
|
||||
};
|
||||
}
|
||||
|
||||
export default function KiloPassMeter({
|
||||
base,
|
||||
bonus,
|
||||
used,
|
||||
total,
|
||||
remaining,
|
||||
nextBillingAt,
|
||||
balance,
|
||||
}: KiloPassMeterProps) {
|
||||
const t = useTranslations("usage");
|
||||
const locale = useLocale();
|
||||
|
||||
const model = useMemo(
|
||||
() => buildKiloPassMeterModel({ base, bonus, used, total, remaining }),
|
||||
[base, bonus, used, total, remaining]
|
||||
);
|
||||
|
||||
const colors = getBarColor(
|
||||
model.total > 0 ? 100 - (model.progressValue / model.total) * 100 : 100
|
||||
);
|
||||
const daysUntilRenewal = calculateDaysUntil(nextBillingAt);
|
||||
const renewalDate = nextBillingAt
|
||||
? new Date(nextBillingAt).toLocaleDateString(locale, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 py-2">
|
||||
{/* Header: Usage / Total */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[12px] font-medium text-text-main">
|
||||
{translateUsageOrFallback(t, "kiloPassUsageLabel", "This month's usage")}
|
||||
</span>
|
||||
<span className="text-[12px] font-bold tabular-nums" style={{ color: colors.text }}>
|
||||
{formatCurrency(model.used)} / {formatCurrency(model.total)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div
|
||||
role="progressbar"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={model.total}
|
||||
aria-valuenow={model.progressValue}
|
||||
aria-valuetext={`${formatCurrency(model.used)} of ${formatCurrency(model.total)}`}
|
||||
aria-label={translateUsageOrFallback(t, "kiloPassMeterLabel", "Kilo Pass usage meter")}
|
||||
className="flex h-3 min-w-0 overflow-hidden rounded-full bg-bg-subtle ring-1 ring-inset ring-border/60"
|
||||
>
|
||||
{model.hasPaidSegment && (
|
||||
<div
|
||||
data-kilo-pass-segment="paid"
|
||||
data-kilo-pass-boundary={model.hasBonusSegment ? "true" : undefined}
|
||||
className={`relative min-w-0 bg-emerald-500/15 ${
|
||||
model.hasBonusSegment ? "border-r-2 border-text-muted" : ""
|
||||
}`}
|
||||
style={{ width: `${model.paidPercent}%` }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 bg-emerald-500 transition-[width] duration-300 ease-out"
|
||||
style={{ width: `${model.paidUsedPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{model.hasBonusSegment && (
|
||||
<div
|
||||
data-kilo-pass-segment="bonus"
|
||||
className="relative min-w-0 bg-sky-500/15"
|
||||
style={{ width: `${model.bonusPercent}%` }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 bg-sky-500 transition-[width] duration-300 ease-out"
|
||||
style={{ width: `${model.bonusUsedPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 items-start text-[11px] text-text-main">
|
||||
{model.hasPaidSegment && (
|
||||
<div className="min-w-0" style={{ width: `${model.paidPercent}%` }}>
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-0.5 leading-4">
|
||||
<span
|
||||
className="inline-block size-2 shrink-0 rounded-full bg-emerald-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{translateUsageOrFallback(t, "kiloPassPaid", "Paid")}</span>
|
||||
</div>
|
||||
<span className="block pt-0.5 font-semibold tabular-nums">
|
||||
{formatCurrency(model.paid)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{model.hasBonusSegment && (
|
||||
<div className="min-w-0" style={{ width: `${model.bonusPercent}%` }}>
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-0.5 leading-4">
|
||||
<span
|
||||
className="inline-block size-2 shrink-0 rounded-full bg-sky-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{translateUsageOrFallback(t, "kiloPassBonus", "Available bonus")}</span>
|
||||
</div>
|
||||
<span className="block pt-0.5 font-semibold tabular-nums">
|
||||
{formatCurrency(model.bonus)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-text-main">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="material-symbols-outlined text-[13px] text-text-muted"
|
||||
aria-hidden="true"
|
||||
>
|
||||
account_balance_wallet
|
||||
</span>
|
||||
<span>{translateUsageOrFallback(t, "kiloPassRemaining", "Remaining")}</span>
|
||||
<span className="font-semibold tabular-nums">{formatCurrency(model.remaining)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Renewal info */}
|
||||
{daysUntilRenewal !== null && renewalDate && (
|
||||
<div className="flex items-center gap-1.5 text-[11px] text-text-muted">
|
||||
<span className="material-symbols-outlined text-[13px]" aria-hidden="true">
|
||||
schedule
|
||||
</span>
|
||||
<span>
|
||||
{translateUsageOrFallback(t, "kiloPassRenews", "Renews in {count} days", {
|
||||
count: daysUntilRenewal,
|
||||
})}
|
||||
</span>
|
||||
<span className="tabular-nums">({renewalDate})</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Account Balance (separate from Kilo Pass) */}
|
||||
{balance !== null && balance !== undefined && (
|
||||
<div className="flex items-center justify-between gap-2 border-t border-border/40 pt-2 text-[11px]">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="material-symbols-outlined text-[13px] text-text-muted"
|
||||
aria-hidden="true"
|
||||
>
|
||||
payments
|
||||
</span>
|
||||
<span className="text-text-main">
|
||||
{translateUsageOrFallback(t, "kiloAccountBalance", "Account Balance")}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-semibold tabular-nums text-text-main">
|
||||
{formatCurrency(balance)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
import { formatQuotaLabel, getBarColor, getQuotaRemainingPercentage, topQuotas } from "../utils";
|
||||
import QuotaMiniBar from "../QuotaMiniBar";
|
||||
import KiloPassMeter from "./KiloPassMeter";
|
||||
import { isKiloPassDisplayRow } from "../quotaParsing";
|
||||
import { translateUsageOrFallback } from "../i18nFallback";
|
||||
|
||||
const CURRENCY_SYMBOLS: Record<string, string> = {
|
||||
@@ -23,6 +25,8 @@ interface Props {
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
message: string | null;
|
||||
/** Collapsed Kilo Pass display row for dedicated meter rendering (kilocode only). */
|
||||
kiloPassRow?: any | null;
|
||||
}
|
||||
|
||||
const MAX_VISIBLE_DEFAULT = 3;
|
||||
@@ -88,6 +92,7 @@ export default function QuotaCardBody({
|
||||
loading,
|
||||
error,
|
||||
message,
|
||||
kiloPassRow = null,
|
||||
}: Props) {
|
||||
const t = useTranslations("usage");
|
||||
|
||||
@@ -128,9 +133,22 @@ export default function QuotaCardBody({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 px-3 pb-2">
|
||||
{visible.map((q, i) => (
|
||||
<QuotaRow key={`${q.name}-${q.modelKey ?? ""}-${i}`} q={q} />
|
||||
))}
|
||||
{visible.map((q, i) =>
|
||||
isKiloPassDisplayRow(q) ? (
|
||||
<KiloPassMeter
|
||||
key={`${q.name}-${q.modelKey ?? ""}-${i}`}
|
||||
base={q.kiloPassBase ?? kiloPassRow?.kiloPassBase ?? 0}
|
||||
bonus={q.kiloPassBonus ?? kiloPassRow?.kiloPassBonus ?? 0}
|
||||
used={q.used}
|
||||
total={q.total}
|
||||
remaining={q.remaining}
|
||||
nextBillingAt={q.resetAt ?? kiloPassRow?.resetAt ?? null}
|
||||
balance={q.kiloPassBalance ?? null}
|
||||
/>
|
||||
) : (
|
||||
<QuotaRow key={`${q.name}-${q.modelKey ?? ""}-${i}`} q={q} />
|
||||
)
|
||||
)}
|
||||
{hidden > 0 && (
|
||||
<div className="text-[10px] text-text-muted italic pt-0.5">+{hidden} more</div>
|
||||
)}
|
||||
|
||||
@@ -19,7 +19,14 @@ import {
|
||||
} from "../utils";
|
||||
import QuotaMiniBar from "../QuotaMiniBar";
|
||||
import { translateUsageOrFallback, type UsageTranslationValues } from "../i18nFallback";
|
||||
import { hasFixedQuotaOrder, hasCanonicalWindowOrder, sortQuotasByWindow } from "../quotaParsing";
|
||||
import {
|
||||
findKiloPassQuotaRow,
|
||||
isKiloPassDisplayRow,
|
||||
hasFixedQuotaOrder,
|
||||
hasCanonicalWindowOrder,
|
||||
sortQuotasByWindow,
|
||||
} from "../quotaParsing";
|
||||
import KiloPassMeter from "./KiloPassMeter";
|
||||
|
||||
const CURRENCY_SYMBOLS: Record<string, string> = {
|
||||
USD: "$",
|
||||
@@ -153,17 +160,34 @@ interface Props {
|
||||
|
||||
function QuotaDetailRow({
|
||||
q,
|
||||
kiloPassRow = null,
|
||||
onHideQuota,
|
||||
onOpenResetCredits,
|
||||
loadingResetCredits = false,
|
||||
}: {
|
||||
q: any;
|
||||
/** Collapsed Kilo Pass display row; present only on kilocode provider cards. */
|
||||
kiloPassRow?: any | null;
|
||||
onHideQuota?: (quota: any) => void;
|
||||
onOpenResetCredits?: () => void;
|
||||
loadingResetCredits?: boolean;
|
||||
}) {
|
||||
const t = useTranslations("usage");
|
||||
const canHide = typeof onHideQuota === "function" && !q.isCredits && !q.isResetCredits;
|
||||
if (isKiloPassDisplayRow(q)) {
|
||||
return (
|
||||
<KiloPassMeter
|
||||
base={q.kiloPassBase ?? kiloPassRow?.kiloPassBase ?? 0}
|
||||
bonus={q.kiloPassBonus ?? kiloPassRow?.kiloPassBonus ?? 0}
|
||||
used={q.used}
|
||||
total={q.total}
|
||||
remaining={q.remaining}
|
||||
nextBillingAt={q.resetAt ?? kiloPassRow?.resetAt ?? null}
|
||||
balance={q.kiloPassBalance ?? null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (q.isResetCredits) {
|
||||
const count = Number(q.creditCount ?? q.remaining ?? 0);
|
||||
const colors = getBarColor(q.remainingPercentage ?? 100);
|
||||
@@ -332,6 +356,10 @@ export default function QuotaCardExpanded({
|
||||
);
|
||||
const hiddenCount = sortedQuotas.length - visibleQuotas.length;
|
||||
|
||||
// Only Kilo Code cards host the dedicated Kilo Pass meter; the collapsed display row carries
|
||||
// the derived meter values while balance/renewal metadata render below it.
|
||||
const kiloPassRow = providerId === "kilocode" ? findKiloPassQuotaRow(sortedQuotas) : null;
|
||||
|
||||
const refreshedLabel = refreshedAt
|
||||
? new Date(refreshedAt).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
@@ -367,6 +395,7 @@ export default function QuotaCardExpanded({
|
||||
<QuotaDetailRow
|
||||
key={`${q.name}-${q.modelKey ?? ""}-${i}`}
|
||||
q={q}
|
||||
kiloPassRow={kiloPassRow}
|
||||
onHideQuota={onHideQuota}
|
||||
onOpenResetCredits={q.isResetCredits ? onOpenResetCredits : undefined}
|
||||
loadingResetCredits={loadingResetCredits}
|
||||
|
||||
@@ -307,6 +307,112 @@ function parseAgentrouter(data: any) {
|
||||
return quotaEntries(data).map(([quotaKey, quota]) => parseAgentrouterQuota(quotaKey, quota));
|
||||
}
|
||||
|
||||
/**
|
||||
* Kilo Code quota parser. Personal balance keeps the credits-style USD row; the four raw Kilo Pass
|
||||
* quota keys (kiloPassBase/kiloPassBonus/kiloPassUsage/kiloPassRemaining) are collapsed into one
|
||||
* display row that carries the real meter semantics: used = currentPeriodUsageUsd, total = base +
|
||||
* bonus, remaining = max(0, total - used). The collapsed row feeds the dedicated KiloPassMeter
|
||||
* component; the raw technical keys must never surface as individual rows because the generic
|
||||
* credits renderer would display creditCount (= remaining) for the usage entry, making "Usage"
|
||||
* read identical to "Remaining".
|
||||
*/
|
||||
const KILO_PASS_DISPLAY_ROW = "kiloPass";
|
||||
|
||||
function kiloNumber(value: any): number {
|
||||
const num = Number(value);
|
||||
return Number.isFinite(num) && num > 0 ? num : 0;
|
||||
}
|
||||
|
||||
function roundKiloCurrency(value: number): number {
|
||||
return Math.round(value * 100) / 100;
|
||||
}
|
||||
|
||||
/** Display-only reset timestamp; invalid input yields null instead of a broken countdown. */
|
||||
function formatKiloResetDate(resetAt: any): string | null {
|
||||
if (typeof resetAt !== "string" || !resetAt.trim()) return null;
|
||||
const parsed = Date.parse(resetAt);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) return null;
|
||||
return new Date(parsed).toISOString();
|
||||
}
|
||||
|
||||
function parseKilocode(data: any) {
|
||||
const rows: any[] = [];
|
||||
let base = 0;
|
||||
let bonus = 0;
|
||||
let usage = 0;
|
||||
let passResetAt: any = null;
|
||||
let balanceRemaining: number | null = null;
|
||||
|
||||
for (const [quotaKey, quota] of quotaEntries(data)) {
|
||||
if (quotaKey === "kiloPassBase") {
|
||||
base = kiloNumber(quota?.total ?? quota?.remaining);
|
||||
passResetAt = passResetAt ?? quota?.resetAt ?? null;
|
||||
continue;
|
||||
}
|
||||
if (quotaKey === "kiloPassBonus") {
|
||||
bonus = kiloNumber(quota?.total ?? quota?.remaining);
|
||||
continue;
|
||||
}
|
||||
if (quotaKey === "kiloPassUsage") {
|
||||
usage = kiloNumber(quota?.used);
|
||||
passResetAt = passResetAt ?? quota?.resetAt ?? null;
|
||||
continue;
|
||||
}
|
||||
if (quotaKey === "kiloPassRemaining") {
|
||||
// Derived value (base + bonus - usage); wire-format only.
|
||||
continue;
|
||||
}
|
||||
if (quotaKey === "balance") {
|
||||
const remaining = kiloNumber(quota?.remaining);
|
||||
balanceRemaining = remaining;
|
||||
const remainingPercentage =
|
||||
safePercentage(quota?.remainingPercentage) ?? (remaining > 0 ? 100 : 0);
|
||||
rows.push(
|
||||
buildCreditsQuota("balance", remaining, remainingPercentage, {
|
||||
currency: quota?.currency || "USD",
|
||||
displayName: quota?.displayName,
|
||||
resetAt: null,
|
||||
unlimited: true,
|
||||
})
|
||||
);
|
||||
continue;
|
||||
}
|
||||
rows.push(normalizeQuotaEntry(quotaKey, quota));
|
||||
}
|
||||
|
||||
const total = roundKiloCurrency(base + bonus);
|
||||
if (total > 0 || usage > 0) {
|
||||
const remaining = Math.max(0, roundKiloCurrency(total - usage));
|
||||
rows.push({
|
||||
name: KILO_PASS_DISPLAY_ROW,
|
||||
displayName: "Kilo Pass",
|
||||
kiloPass: true,
|
||||
kiloPassBase: base,
|
||||
kiloPassBonus: bonus,
|
||||
...(balanceRemaining !== null ? { kiloPassBalance: balanceRemaining } : {}),
|
||||
used: usage,
|
||||
total,
|
||||
remaining,
|
||||
remainingPercentage: total > 0 ? Math.max(0, (remaining / total) * 100) : 0,
|
||||
resetAt: formatKiloResetDate(passResetAt),
|
||||
unlimited: false,
|
||||
currency: "USD",
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** Finds the collapsed Kilo Pass display row within parsed quota rows, if present. */
|
||||
export function findKiloPassQuotaRow(quotas: any[] | undefined | null): any | null {
|
||||
if (!Array.isArray(quotas)) return null;
|
||||
return quotas.find((quota) => quota?.kiloPass === true) ?? null;
|
||||
}
|
||||
|
||||
export function isKiloPassDisplayRow(quota: any): boolean {
|
||||
return quota?.kiloPass === true || quota?.name === KILO_PASS_DISPLAY_ROW;
|
||||
}
|
||||
|
||||
function parseProviderQuotas(providerId: string, data: any) {
|
||||
if (providerId === "github") return parseGithub(data);
|
||||
if (["glm", "glm-cn", "glmt", "opencode-go"].includes(providerId)) return parseGlmFamily(data);
|
||||
@@ -314,6 +420,7 @@ function parseProviderQuotas(providerId: string, data: any) {
|
||||
if (providerId === "codex") return parseCodex(data);
|
||||
if (providerId === "claude") return parseClaude(data);
|
||||
if (providerId === "deepseek") return parseDeepseek(data);
|
||||
if (providerId === "kilocode") return parseKilocode(data);
|
||||
if (providerId === "agentrouter") return parseAgentrouter(data);
|
||||
return parseGeneric(data);
|
||||
}
|
||||
|
||||
@@ -9230,6 +9230,13 @@
|
||||
"kimiExtraUsageFrozen": "Frozen",
|
||||
"kimiExtraUsageUnavailable": "Unavailable",
|
||||
"kimiMonthlyUsed": "Used this month",
|
||||
"kiloAccountBalance": "Kontoguthaben",
|
||||
"kiloPassBonus": "Verfügbarer Bonus",
|
||||
"kiloPassMeterLabel": "Kilo-Pass-Nutzungsanzeige",
|
||||
"kiloPassPaid": "Bezahlt",
|
||||
"kiloPassRemaining": "Verbleibend",
|
||||
"kiloPassRenews": "Erneuert sich in {count} Tagen",
|
||||
"kiloPassUsageLabel": "Nutzung diesen Monat",
|
||||
"kimiMonthlyLimit": "Monthly limit",
|
||||
"kimiMonthlyLimitUnlimited": "Unlimited",
|
||||
"kimiAdditionalCredits": "Additional Credits",
|
||||
|
||||
@@ -9219,6 +9219,13 @@
|
||||
"grokAutoTopUpMax": "max",
|
||||
"grokAutoTopUpMonth": "month",
|
||||
"grokAdditionalCredits": "Additional Credits",
|
||||
"kiloAccountBalance": "Account Balance",
|
||||
"kiloPassBonus": "Available bonus",
|
||||
"kiloPassMeterLabel": "Kilo Pass usage meter",
|
||||
"kiloPassPaid": "Paid",
|
||||
"kiloPassRemaining": "Remaining",
|
||||
"kiloPassRenews": "Renews {count} days",
|
||||
"kiloPassUsageLabel": "This month's usage",
|
||||
"kimiExtraUsageCredits": "Extra Usage Credits",
|
||||
"kimiExtraUsage": "Extra Usage",
|
||||
"kimiExtraUsageEnabled": "Enabled",
|
||||
|
||||
@@ -9220,6 +9220,13 @@
|
||||
"grokAutoTopUpMax": "máximo",
|
||||
"grokAutoTopUpMonth": "mês",
|
||||
"grokAdditionalCredits": "Créditos adicionais",
|
||||
"kiloAccountBalance": "__MISSING__:Account Balance",
|
||||
"kiloPassBonus": "__MISSING__:Available bonus",
|
||||
"kiloPassMeterLabel": "__MISSING__:Kilo Pass usage meter",
|
||||
"kiloPassPaid": "__MISSING__:Paid",
|
||||
"kiloPassRemaining": "__MISSING__:Remaining",
|
||||
"kiloPassRenews": "__MISSING__:Renews {count} days",
|
||||
"kiloPassUsageLabel": "__MISSING__:This month's usage",
|
||||
"kimiExtraUsageCredits": "Créditos de uso extra",
|
||||
"kimiExtraUsage": "Uso extra",
|
||||
"kimiExtraUsageEnabled": "Ativado",
|
||||
@@ -12957,6 +12964,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"combo": {
|
||||
"sort": {
|
||||
"label": "Ordenar por",
|
||||
"method": {
|
||||
"manual": "Manual",
|
||||
"provider": "Provedor",
|
||||
"score": "Pontuação (modelos gratuitos)",
|
||||
"name": "Nome"
|
||||
},
|
||||
"scoreHint": "A ordenação por pontuação vale só para provedores gratuitos; os demais ficam onde estão."
|
||||
}
|
||||
},
|
||||
"comboControl": {
|
||||
"title": "Central de Controle de Combo",
|
||||
"unavailable": "Central de Controle de Combo indisponível",
|
||||
@@ -13978,17 +13997,5 @@
|
||||
"cta": "Obter uma chave de API",
|
||||
"partnerLinkNote": "Link de parceiro",
|
||||
"dismissAriaLabel": "Dispensar"
|
||||
},
|
||||
"combo": {
|
||||
"sort": {
|
||||
"label": "Ordenar por",
|
||||
"method": {
|
||||
"manual": "Manual",
|
||||
"provider": "Provedor",
|
||||
"score": "Pontuação (modelos gratuitos)",
|
||||
"name": "Nome"
|
||||
},
|
||||
"scoreHint": "A ordenação por pontuação vale só para provedores gratuitos; os demais ficam onde estão."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9220,6 +9220,13 @@
|
||||
"grokAutoTopUpMax": "tối đa",
|
||||
"grokAutoTopUpMonth": "tháng",
|
||||
"grokAdditionalCredits": "Tín dụng bổ sung",
|
||||
"kiloAccountBalance": "Số dư tài khoản",
|
||||
"kiloPassBonus": "Bonus khả dụng",
|
||||
"kiloPassMeterLabel": "Đồng hồ mức sử dụng Kilo Pass",
|
||||
"kiloPassPaid": "Đã thanh toán",
|
||||
"kiloPassRemaining": "Còn lại",
|
||||
"kiloPassRenews": "Gia hạn sau {count} ngày",
|
||||
"kiloPassUsageLabel": "Mức sử dụng tháng này",
|
||||
"kimiExtraUsageCredits": "Tín dụng sử dụng bổ sung",
|
||||
"kimiExtraUsage": "Sử dụng bổ sung",
|
||||
"kimiExtraUsageEnabled": "Đã bật",
|
||||
@@ -11286,16 +11293,16 @@
|
||||
"detail": {
|
||||
"collapse": "Thu gọn {title}",
|
||||
"expand": "Mở rộng {title}",
|
||||
"collapseAllLevels": "Thu gọn tất cả",
|
||||
"collapseOneLevel": "Thu gọn một cấp",
|
||||
"currentExpandLevel": "Cấp mở rộng hiện tại",
|
||||
"expandOneLevel": "Mở rộng một cấp",
|
||||
"expandAllLevels": "Mở rộng tất cả",
|
||||
"copyTitle": "Sao chép {title}",
|
||||
"copied": "Đã sao chép!",
|
||||
"copy": "Sao chép",
|
||||
"autoscrollOn": "Tự động cuộn: bật",
|
||||
"autoscrollOff": "Tự động cuộn: tắt",
|
||||
"collapseAllLevels": "Thu gọn tất cả",
|
||||
"collapseOneLevel": "Thu gọn một cấp",
|
||||
"currentExpandLevel": "Cấp mở rộng hiện tại",
|
||||
"expandOneLevel": "Mở rộng một cấp",
|
||||
"expandAllLevels": "Mở rộng tất cả",
|
||||
"payload": {
|
||||
"clientRawRequest": "Yêu cầu thô của client",
|
||||
"clientRequest": "Yêu cầu client",
|
||||
@@ -12957,6 +12964,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"combo": {
|
||||
"sort": {
|
||||
"label": "Sắp xếp theo",
|
||||
"method": {
|
||||
"manual": "Thủ công",
|
||||
"provider": "Nhà cung cấp",
|
||||
"score": "Điểm (mô hình miễn phí)",
|
||||
"name": "Tên"
|
||||
},
|
||||
"scoreHint": "Xếp hạng theo điểm chỉ áp dụng cho nhà cung cấp miễn phí; các nhà cung cấp khác giữ nguyên vị trí."
|
||||
}
|
||||
},
|
||||
"comboControl": {
|
||||
"title": "Trung tâm điều khiển combo",
|
||||
"unavailable": "Trung tâm điều khiển combo không khả dụng",
|
||||
@@ -13978,17 +13997,5 @@
|
||||
"cta": "Lấy khóa API",
|
||||
"partnerLinkNote": "Liên kết đối tác",
|
||||
"dismissAriaLabel": "Đóng"
|
||||
},
|
||||
"combo": {
|
||||
"sort": {
|
||||
"label": "Sắp xếp theo",
|
||||
"method": {
|
||||
"manual": "Thủ công",
|
||||
"provider": "Nhà cung cấp",
|
||||
"score": "Điểm (mô hình miễn phí)",
|
||||
"name": "Tên"
|
||||
},
|
||||
"scoreHint": "Xếp hạng theo điểm chỉ áp dụng cho nhà cung cấp miễn phí; các nhà cung cấp khác giữ nguyên vị trí."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,6 +539,8 @@ export const USAGE_SUPPORTED_PROVIDERS = [
|
||||
"qwen-cloud-token-plan",
|
||||
// AgentRouter (New-API) console balance quota (consoleApiKey + newApiUserId)
|
||||
"agentrouter",
|
||||
// Kilo Code personal USD balance (GET /api/profile/balance, existing OAuth token)
|
||||
"kilocode",
|
||||
];
|
||||
|
||||
// ── Zod validation, lazily on first AI_PROVIDERS access (perf: skips the walk
|
||||
|
||||
247
tests/unit/kilo-pass-meter-rendering.test.tsx
Normal file
247
tests/unit/kilo-pass-meter-rendering.test.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
// @vitest-environment jsdom
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { parseQuotaData } from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/quotaParsing";
|
||||
import QuotaCardExpanded from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded";
|
||||
import KiloPassMeter, {
|
||||
buildKiloPassMeterModel,
|
||||
} from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/KiloPassMeter";
|
||||
|
||||
vi.mock("next-intl", () => ({
|
||||
useLocale: () => "en-US",
|
||||
useTranslations: () =>
|
||||
Object.assign(
|
||||
(key: string, values?: { count?: number; pct?: number }) => {
|
||||
if (key === "kiloPassUsageLabel") return "This month's usage";
|
||||
if (key === "kiloPassPaid") return "Paid";
|
||||
if (key === "kiloPassBonus") return "Available bonus";
|
||||
if (key === "kiloPassRemaining") return "Remaining";
|
||||
if (key === "kiloAccountBalance") return "Account Balance";
|
||||
if (key === "percentLeft") return `${values?.pct}% left`;
|
||||
return key;
|
||||
},
|
||||
{ has: () => true }
|
||||
),
|
||||
}));
|
||||
|
||||
const rawKilocodeUsage = {
|
||||
quotas: {
|
||||
balance: {
|
||||
remaining: 11.51,
|
||||
remainingPercentage: 100,
|
||||
currency: "USD",
|
||||
displayName: "Personal Balance",
|
||||
unlimited: true,
|
||||
},
|
||||
kiloPassBase: {
|
||||
total: 49,
|
||||
remaining: 49,
|
||||
resetAt: "2030-09-15T00:00:00.000Z",
|
||||
},
|
||||
kiloPassBonus: { total: 24.5, remaining: 24.5 },
|
||||
kiloPassUsage: {
|
||||
used: 73.55,
|
||||
total: 73.5,
|
||||
resetAt: "2030-09-15T00:00:00.000Z",
|
||||
},
|
||||
kiloPassRemaining: { remaining: 0, total: 73.5 },
|
||||
},
|
||||
};
|
||||
|
||||
describe("Kilo Pass meter production render path", () => {
|
||||
it("renders the dedicated meter rather than a generic 0% left quota row", () => {
|
||||
const quotas = parseQuotaData("kilocode", rawKilocodeUsage);
|
||||
const kiloPass = quotas.find((quota) => quota.kiloPass);
|
||||
|
||||
expect(kiloPass).toMatchObject({
|
||||
name: "kiloPass",
|
||||
kiloPass: true,
|
||||
kiloPassBase: 49,
|
||||
kiloPassBonus: 24.5,
|
||||
used: 73.55,
|
||||
total: 73.5,
|
||||
remaining: 0,
|
||||
kiloPassBalance: 11.51,
|
||||
});
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
<QuotaCardExpanded
|
||||
quotas={quotas}
|
||||
providerId="kilocode"
|
||||
loading={false}
|
||||
error={null}
|
||||
hasStaleData={false}
|
||||
onRefresh={() => {}}
|
||||
onOpenCutoff={() => {}}
|
||||
onOpenCost={() => {}}
|
||||
canEditCutoff={false}
|
||||
hasCutoffOverrides={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(html).toContain("This month's usage");
|
||||
expect(html).toContain("Paid");
|
||||
expect(html).toContain("Available bonus");
|
||||
expect(html).toContain("Remaining");
|
||||
expect(html).toContain("Account Balance");
|
||||
expect(html).toContain("$73.55");
|
||||
expect(html).toContain("$73.50");
|
||||
expect(html).toContain('aria-valuemax="73.5"');
|
||||
expect(html).toContain('aria-valuenow="73.5"');
|
||||
expect(html).not.toContain("0% left");
|
||||
});
|
||||
});
|
||||
|
||||
const renderMeter = (props: Partial<React.ComponentProps<typeof KiloPassMeter>> = {}) =>
|
||||
renderToStaticMarkup(
|
||||
<KiloPassMeter
|
||||
base={49}
|
||||
bonus={24.5}
|
||||
used={47.95}
|
||||
total={73.5}
|
||||
remaining={25.55}
|
||||
balance={11.51}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
describe("KiloPassMeter segment model", () => {
|
||||
it("splits the pool into paid and bonus segments and uses paid credits first", () => {
|
||||
const model = buildKiloPassMeterModel({
|
||||
base: 49,
|
||||
bonus: 24.5,
|
||||
used: 47.95,
|
||||
total: 73.5,
|
||||
remaining: 25.55,
|
||||
});
|
||||
|
||||
expect(model.paidPercent).toBeCloseTo(66.67, 1);
|
||||
expect(model.bonusPercent).toBeCloseTo(33.33, 1);
|
||||
expect(model.paidUsedPercent).toBeCloseTo(97.86, 1);
|
||||
expect(model.bonusUsedPercent).toBe(0);
|
||||
});
|
||||
|
||||
it("fills paid completely before consuming the bonus segment", () => {
|
||||
const model = buildKiloPassMeterModel({
|
||||
base: 49,
|
||||
bonus: 24.5,
|
||||
used: 60,
|
||||
total: 73.5,
|
||||
remaining: 13.5,
|
||||
});
|
||||
|
||||
expect(model.paidUsedPercent).toBe(100);
|
||||
expect(model.bonusUsedPercent).toBeCloseTo(44.9, 1);
|
||||
});
|
||||
|
||||
it("fills both segments exactly when usage reaches the total", () => {
|
||||
const model = buildKiloPassMeterModel({
|
||||
base: 49,
|
||||
bonus: 24.5,
|
||||
used: 73.5,
|
||||
total: 73.5,
|
||||
remaining: 0,
|
||||
});
|
||||
|
||||
expect(model.paidUsedPercent).toBe(100);
|
||||
expect(model.bonusUsedPercent).toBe(100);
|
||||
expect(model.progressValue).toBe(73.5);
|
||||
});
|
||||
|
||||
it("clamps only visual progress when usage exceeds the total", () => {
|
||||
const model = buildKiloPassMeterModel({
|
||||
base: 49,
|
||||
bonus: 24.5,
|
||||
used: 73.55,
|
||||
total: 73.5,
|
||||
remaining: 0,
|
||||
});
|
||||
|
||||
expect(model.used).toBe(73.55);
|
||||
expect(model.progressValue).toBe(73.5);
|
||||
expect(model.paidUsedPercent).toBe(100);
|
||||
expect(model.bonusUsedPercent).toBe(100);
|
||||
});
|
||||
|
||||
it("handles absent paid or bonus pools and zero totals without NaN percentages", () => {
|
||||
const onlyPaid = buildKiloPassMeterModel({
|
||||
base: 49,
|
||||
bonus: 0,
|
||||
used: 10,
|
||||
total: 49,
|
||||
remaining: 39,
|
||||
});
|
||||
const onlyBonus = buildKiloPassMeterModel({
|
||||
base: 0,
|
||||
bonus: 24.5,
|
||||
used: 10,
|
||||
total: 24.5,
|
||||
remaining: 14.5,
|
||||
});
|
||||
const empty = buildKiloPassMeterModel({
|
||||
base: 0,
|
||||
bonus: 0,
|
||||
used: 0,
|
||||
total: 0,
|
||||
remaining: 0,
|
||||
});
|
||||
|
||||
expect(onlyPaid.bonusPercent).toBe(0);
|
||||
expect(onlyBonus.paidPercent).toBe(0);
|
||||
expect(empty.paidPercent).toBe(0);
|
||||
expect(empty.bonusPercent).toBe(0);
|
||||
expect(empty.paidUsedPercent).toBe(0);
|
||||
expect(empty.bonusUsedPercent).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("KiloPassMeter segmented presentation", () => {
|
||||
it("renders two labeled segments, their boundary, semantic usage, and separate account balance", () => {
|
||||
const html = renderMeter();
|
||||
|
||||
expect(html).toContain('data-kilo-pass-segment="paid"');
|
||||
expect(html).toContain('data-kilo-pass-segment="bonus"');
|
||||
expect(html).toContain('data-kilo-pass-boundary="true"');
|
||||
expect(html).toContain('aria-valuemin="0"');
|
||||
expect(html).toContain('aria-valuemax="73.5"');
|
||||
expect(html).toContain('aria-valuenow="47.95"');
|
||||
expect(html).toContain("Paid");
|
||||
expect(html).toContain("Available bonus");
|
||||
expect(html).toContain("Account Balance");
|
||||
expect(html).toContain("$11.51");
|
||||
expect(html).not.toContain("kiloPassBase");
|
||||
expect(html).not.toContain("kiloPassBonus");
|
||||
expect(html).not.toContain("kiloPassUsage");
|
||||
});
|
||||
|
||||
it("omits the bonus segment and boundary when no free bonus exists", () => {
|
||||
const html = renderMeter({ bonus: 0, total: 49, used: 10, remaining: 39 });
|
||||
|
||||
expect(html).toContain('data-kilo-pass-segment="paid"');
|
||||
expect(html).not.toContain('data-kilo-pass-segment="bonus"');
|
||||
expect(html).not.toContain('data-kilo-pass-boundary="true"');
|
||||
});
|
||||
|
||||
it("omits the paid segment when only the free bonus pool exists", () => {
|
||||
const html = renderMeter({ base: 0, bonus: 24.5, total: 24.5, used: 10, remaining: 14.5 });
|
||||
|
||||
expect(html).not.toContain('data-kilo-pass-segment="paid"');
|
||||
expect(html).toContain('data-kilo-pass-segment="bonus"');
|
||||
});
|
||||
|
||||
it("keeps the real overage in text while clamping its accessible progress value", () => {
|
||||
const html = renderMeter({ used: 73.55, remaining: 0 });
|
||||
|
||||
expect(html).toContain("$73.55 / $73.50");
|
||||
expect(html).toContain('aria-valuenow="73.5"');
|
||||
});
|
||||
|
||||
it("renders a stable empty meter for a zero total", () => {
|
||||
const html = renderMeter({ base: 0, bonus: 0, used: 0, total: 0, remaining: 0, balance: null });
|
||||
|
||||
expect(html).toContain('aria-valuemax="0"');
|
||||
expect(html).toContain('aria-valuenow="0"');
|
||||
expect(html).not.toContain("NaN");
|
||||
});
|
||||
});
|
||||
261
tests/unit/kilocode-usage-wiring.test.ts
Normal file
261
tests/unit/kilocode-usage-wiring.test.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* Kilo Code usage wiring tests: provider visibility (USAGE_SUPPORTED_PROVIDERS),
|
||||
* fetcher registration (USAGE_FETCHER_PROVIDERS), dispatcher routing in
|
||||
* getUsageForProvider(), and the Dashboard quota parser (kilocode is rendered
|
||||
* through the AgentRouter/USD-credit parser so the exact dollar balance is
|
||||
* shown instead of a bare percentage).
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { USAGE_SUPPORTED_PROVIDERS } from "../../src/shared/constants/providers.ts";
|
||||
import { supportsProviderQuota } from "../../src/shared/utils/providerQuotaVisibility.ts";
|
||||
import { USAGE_FETCHER_PROVIDERS, getUsageForProvider } from "../../open-sse/services/usage.ts";
|
||||
import {
|
||||
findKiloPassQuotaRow,
|
||||
parseQuotaData,
|
||||
} from "../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/quotaParsing.ts";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
test.afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
function mockBalance(status: number, body: unknown) {
|
||||
globalThis.fetch = (async () => {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
}
|
||||
|
||||
test("kilocode is registered in USAGE_SUPPORTED_PROVIDERS", () => {
|
||||
assert.equal(
|
||||
USAGE_SUPPORTED_PROVIDERS.includes("kilocode" as (typeof USAGE_SUPPORTED_PROVIDERS)[number]),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("supportsProviderQuota('kilocode') is true", () => {
|
||||
assert.equal(supportsProviderQuota("kilocode"), true);
|
||||
});
|
||||
|
||||
test("kilocode is registered in USAGE_FETCHER_PROVIDERS", () => {
|
||||
assert.equal(
|
||||
USAGE_FETCHER_PROVIDERS.includes("kilocode" as (typeof USAGE_FETCHER_PROVIDERS)[number]),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("getUsageForProvider dispatches kilocode to the Kilo balance fetcher", async () => {
|
||||
mockBalance(200, { balance: 12.34 });
|
||||
|
||||
const usage = (await getUsageForProvider({
|
||||
id: "conn-kilo",
|
||||
provider: "kilocode",
|
||||
accessToken: "oauth-token-123",
|
||||
})) as {
|
||||
plan?: string;
|
||||
quotas?: Record<
|
||||
string,
|
||||
{ remaining?: number; currency?: string; displayName?: string; remainingPercentage?: number }
|
||||
>;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
assert.equal(usage.plan, "Kilo Code");
|
||||
assert.ok(usage.quotas);
|
||||
const balance = usage.quotas.balance;
|
||||
assert.ok(balance, "expected a balance quota entry");
|
||||
assert.equal(balance.remaining, 12.34);
|
||||
assert.equal(balance.currency, "USD");
|
||||
assert.equal(balance.displayName, "Balance (USD)");
|
||||
assert.equal(usage.message, undefined);
|
||||
});
|
||||
|
||||
test("kilocode balance quota parses as USD credit row in the Dashboard parser", async () => {
|
||||
const usage = {
|
||||
plan: "Kilo Code",
|
||||
quotas: {
|
||||
balance: {
|
||||
used: 0,
|
||||
total: 0,
|
||||
remaining: 12.34,
|
||||
remainingPercentage: 100,
|
||||
resetAt: null,
|
||||
unlimited: true,
|
||||
currency: "USD",
|
||||
displayName: "Balance (USD)",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const rows = parseQuotaData("kilocode", usage) as Array<{
|
||||
isCredits?: boolean;
|
||||
currency?: string;
|
||||
creditCount?: number;
|
||||
remainingPercentage?: number;
|
||||
}>;
|
||||
|
||||
assert.equal(rows.length, 1);
|
||||
const [row] = rows;
|
||||
assert.equal(row.isCredits, true, "renderer only formats USD when isCredits is true");
|
||||
assert.equal(row.currency, "USD", "renderer looks up CURRENCY_SYMBOLS[q.currency]");
|
||||
assert.equal(row.creditCount, 12.34, "renderer displays q.creditCount as the dollar amount");
|
||||
assert.equal(row.remainingPercentage, 100, "funded wallet must not read as exhausted");
|
||||
});
|
||||
|
||||
test("kilocode balance + Kilo Pass quotas collapse into one meter row (balance untouched)", async () => {
|
||||
const balanceQuota = {
|
||||
used: 0,
|
||||
total: 0,
|
||||
remaining: 11.51,
|
||||
remainingPercentage: 100,
|
||||
resetAt: null,
|
||||
unlimited: true,
|
||||
currency: "USD",
|
||||
displayName: "Balance (USD)",
|
||||
};
|
||||
const baseQuota = {
|
||||
used: 0,
|
||||
total: 50,
|
||||
remaining: 50,
|
||||
remainingPercentage: 100,
|
||||
resetAt: "2026-09-15T00:00:00.000Z",
|
||||
unlimited: false,
|
||||
currency: "USD",
|
||||
displayName: "Base Credits",
|
||||
};
|
||||
const bonusQuota = {
|
||||
used: 0,
|
||||
total: 5,
|
||||
remaining: 5,
|
||||
remainingPercentage: 100,
|
||||
resetAt: null,
|
||||
unlimited: false,
|
||||
currency: "USD",
|
||||
displayName: "Bonus Credits",
|
||||
};
|
||||
const usageQuota = {
|
||||
used: 30,
|
||||
total: 55,
|
||||
remaining: 25,
|
||||
remainingPercentage: 45.45,
|
||||
resetAt: "2026-09-15T00:00:00.000Z",
|
||||
unlimited: false,
|
||||
currency: "USD",
|
||||
displayName: "Kilo Pass Usage",
|
||||
};
|
||||
const remainingQuota = {
|
||||
used: 0,
|
||||
total: 0,
|
||||
remaining: 25,
|
||||
remainingPercentage: 100,
|
||||
resetAt: "2026-09-15T00:00:00.000Z",
|
||||
unlimited: false,
|
||||
currency: "USD",
|
||||
displayName: "Pass Remaining",
|
||||
};
|
||||
const usage = {
|
||||
plan: "Kilo Code",
|
||||
quotas: {
|
||||
balance: balanceQuota,
|
||||
kiloPassBase: baseQuota,
|
||||
kiloPassBonus: bonusQuota,
|
||||
kiloPassUsage: usageQuota,
|
||||
kiloPassRemaining: remainingQuota,
|
||||
},
|
||||
};
|
||||
|
||||
const rows = parseQuotaData("kilocode", usage) as Array<{
|
||||
name?: string;
|
||||
isCredits?: boolean;
|
||||
currency?: string;
|
||||
creditCount?: number;
|
||||
displayName?: string;
|
||||
unlimited?: boolean;
|
||||
kiloPass?: boolean;
|
||||
kiloPassBase?: number;
|
||||
kiloPassBonus?: number;
|
||||
kiloPassBalance?: number;
|
||||
used?: number;
|
||||
total?: number;
|
||||
remaining?: number;
|
||||
remainingPercentage?: number;
|
||||
resetAt?: string | null;
|
||||
}>;
|
||||
assert.equal(
|
||||
rows.length,
|
||||
2,
|
||||
"four raw Kilo Pass keys must collapse into one meter row; balance stays separate"
|
||||
);
|
||||
|
||||
const balance = rows.find((r) => r.name === "balance");
|
||||
assert.ok(balance, "personal balance row must survive");
|
||||
assert.equal(balance?.isCredits, true);
|
||||
assert.equal(balance?.currency, "USD");
|
||||
assert.equal(balance?.creditCount, 11.51);
|
||||
assert.equal(balance?.unlimited, true);
|
||||
|
||||
const pass = findKiloPassQuotaRow(rows);
|
||||
assert.ok(pass, "collapsed Kilo Pass row must be discoverable via findKiloPassQuotaRow");
|
||||
assert.equal(pass?.kiloPass, true);
|
||||
assert.equal(pass?.displayName, "Kilo Pass");
|
||||
assert.equal(pass?.currency, "USD");
|
||||
assert.equal(pass?.kiloPassBase, 50);
|
||||
assert.equal(pass?.kiloPassBonus, 5);
|
||||
assert.equal(pass?.kiloPassBalance, 11.51, "wallet balance rides along for meter footer");
|
||||
assert.equal(pass?.used, 30);
|
||||
assert.equal(pass?.total, 55, "meter total must be base + bonus");
|
||||
assert.equal(pass?.remaining, 25);
|
||||
assert.equal(pass?.remainingPercentage, (25 / 55) * 100);
|
||||
assert.equal(pass?.resetAt, "2026-09-15T00:00:00.000Z");
|
||||
assert.equal(pass?.unlimited, false);
|
||||
});
|
||||
|
||||
test("getUsageForProvider returns Kilo Pass quotas through dispatcher", async () => {
|
||||
const subscription = {
|
||||
status: "active",
|
||||
currentPeriodBaseCreditsUsd: 50,
|
||||
currentPeriodUsageUsd: 30,
|
||||
currentPeriodBonusCreditsUsd: 5,
|
||||
nextBillingAt: "2026-09-15T00:00:00.000Z",
|
||||
};
|
||||
const json = { subscription };
|
||||
const data = { json };
|
||||
const result = { data };
|
||||
const passPayload = [{ result }];
|
||||
|
||||
globalThis.fetch = (async (url: unknown) => {
|
||||
const u = String(url);
|
||||
if (u.includes("/api/profile/balance")) {
|
||||
return new Response(JSON.stringify({ balance: 11.51 }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new Response(JSON.stringify(passPayload), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
const usage = (await getUsageForProvider({
|
||||
id: "conn-kilo-pass",
|
||||
provider: "kilocode",
|
||||
accessToken: "oauth-token-123",
|
||||
})) as {
|
||||
plan?: string;
|
||||
quotas?: Record<string, { remaining?: number; used?: number }>;
|
||||
message?: string;
|
||||
};
|
||||
assert.equal(usage.plan, "Kilo Code");
|
||||
assert.equal(usage.message, undefined);
|
||||
assert.ok(usage.quotas);
|
||||
assert.equal(usage.quotas.balance.remaining, 11.51);
|
||||
assert.equal(usage.quotas.kiloPassBase.remaining, 50);
|
||||
assert.equal(usage.quotas.kiloPassUsage.used, 30);
|
||||
assert.equal(usage.quotas.kiloPassRemaining.remaining, 25);
|
||||
});
|
||||
512
tests/unit/kilocode-usage.test.ts
Normal file
512
tests/unit/kilocode-usage.test.ts
Normal file
@@ -0,0 +1,512 @@
|
||||
/**
|
||||
* Kilo Code balance + Kilo Pass usage fetcher tests.
|
||||
* Covers OAuth-only credential resolution, anonymous-token rejection,
|
||||
* KILO_API_URL override, balance parsing, Kilo Pass parsing, partial
|
||||
* failures between the two endpoints, and quota building.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
getKilocodeUsage,
|
||||
parseKilocodeBalance,
|
||||
parseKiloPassState,
|
||||
buildKilocodeUsageResult,
|
||||
buildKiloPassUsageResult,
|
||||
} from "../../open-sse/services/usage/kilocode.ts";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
test.afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
interface RecordedCall {
|
||||
url: string;
|
||||
init: RequestInit;
|
||||
}
|
||||
|
||||
function jsonResponse(body: unknown, status: number): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dual fetch recorder: balance endpoint gets balanceBody/balanceStatus,
|
||||
* Kilo Pass endpoint gets passBody/passStatus.
|
||||
*/
|
||||
function recordDualFetch(
|
||||
balanceBody: unknown,
|
||||
balanceStatus: number,
|
||||
passBody: unknown,
|
||||
passStatus: number
|
||||
): Array<RecordedCall> {
|
||||
const calls: Array<RecordedCall> = [];
|
||||
globalThis.fetch = async (url: unknown, init: unknown) => {
|
||||
const u = String(url);
|
||||
calls.push({ url: u, init: (init ?? {}) as RequestInit });
|
||||
const isBalance = u.includes("/api/profile/balance");
|
||||
return isBalance
|
||||
? jsonResponse(balanceBody, balanceStatus)
|
||||
: jsonResponse(passBody, passStatus);
|
||||
};
|
||||
return calls;
|
||||
}
|
||||
|
||||
function activePassPayload(): unknown {
|
||||
const subscription: Record<string, unknown> = {
|
||||
tier: "tier_19",
|
||||
status: "active",
|
||||
currentPeriodBaseCreditsUsd: 50,
|
||||
currentPeriodUsageUsd: 30,
|
||||
currentPeriodBonusCreditsUsd: 5,
|
||||
nextBillingAt: "2026-09-15T00:00:00.000Z",
|
||||
};
|
||||
const json: Record<string, unknown> = { subscription };
|
||||
const data: Record<string, unknown> = { json };
|
||||
const result: Record<string, unknown> = { data };
|
||||
return [{ result }];
|
||||
}
|
||||
|
||||
const PASS_ACTIVE = activePassPayload();
|
||||
const baseConnection: Record<string, unknown> = {
|
||||
provider: "kilocode",
|
||||
accessToken: "oauth-token-123",
|
||||
};
|
||||
|
||||
interface QuotaEntry {
|
||||
remaining?: number;
|
||||
used?: number;
|
||||
total?: number;
|
||||
remainingPercentage?: number;
|
||||
resetAt?: string | null;
|
||||
currency?: string;
|
||||
displayName?: string;
|
||||
unlimited?: boolean;
|
||||
}
|
||||
|
||||
interface UsageResult {
|
||||
plan?: string;
|
||||
quotas?: Record<string, QuotaEntry>;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// OAuth-only credential tests
|
||||
|
||||
test("no OAuth access token: no fetch, readable message", async () => {
|
||||
const calls = recordDualFetch({ balance: 1 }, 200, PASS_ACTIVE, 200);
|
||||
const usage = (await getKilocodeUsage("conn-none", { provider: "kilocode" })) as UsageResult;
|
||||
assert.equal(calls.length, 0);
|
||||
assert.ok(typeof usage.message === "string" && usage.message.length > 0);
|
||||
});
|
||||
|
||||
test("anonymous token: no fetch, readable message", async () => {
|
||||
const calls = recordDualFetch({ balance: 1 }, 200, PASS_ACTIVE, 200);
|
||||
const usage = (await getKilocodeUsage("conn-anon", {
|
||||
provider: "kilocode",
|
||||
accessToken: "anonymous",
|
||||
})) as UsageResult;
|
||||
assert.equal(calls.length, 0);
|
||||
assert.ok(typeof usage.message === "string");
|
||||
});
|
||||
|
||||
test("apiKey without OAuth accessToken: no fetch, API key not used", async () => {
|
||||
const calls = recordDualFetch({ balance: 1 }, 200, PASS_ACTIVE, 200);
|
||||
const usage = (await getKilocodeUsage("conn-apikey", {
|
||||
provider: "kilocode",
|
||||
apiKey: "kilo-pat-999",
|
||||
})) as UsageResult;
|
||||
assert.equal(calls.length, 0);
|
||||
assert.ok(/OAuth/i.test(usage.message ?? ""));
|
||||
});
|
||||
|
||||
test("apiKey with OAuth accessToken: OAuth token wins", async () => {
|
||||
const calls = recordDualFetch({ balance: 7.5 }, 200, PASS_ACTIVE, 200);
|
||||
await getKilocodeUsage("conn-both", {
|
||||
provider: "kilocode",
|
||||
accessToken: "oauth-token-123",
|
||||
apiKey: "kilo-pat-999",
|
||||
});
|
||||
assert.equal(calls.length, 2);
|
||||
for (const call of calls) {
|
||||
const headers = call.init.headers as Record<string, string>;
|
||||
assert.equal(headers.Authorization, "Bearer oauth-token-123");
|
||||
}
|
||||
});
|
||||
|
||||
// Success: both endpoints
|
||||
|
||||
test("success: balance + Kilo Pass both present", async () => {
|
||||
const calls = recordDualFetch({ balance: 12.34 }, 200, PASS_ACTIVE, 200);
|
||||
const usage = (await getKilocodeUsage("conn-1", baseConnection)) as UsageResult;
|
||||
assert.equal(usage.plan, "Kilo Code");
|
||||
assert.equal(calls.length, 2);
|
||||
const urls = calls.map((c) => c.url);
|
||||
assert.ok(urls.some((u) => u.includes("/api/profile/balance")));
|
||||
assert.ok(urls.some((u) => u.includes("/api/trpc/kiloPass.getState")));
|
||||
for (const call of calls) {
|
||||
const headers = call.init.headers as Record<string, string>;
|
||||
assert.equal(headers.Authorization, "Bearer oauth-token-123");
|
||||
assert.equal(headers["X-KILOCODE-EDITORNAME"], "OmniRoute");
|
||||
}
|
||||
const quotas = usage.quotas ?? {};
|
||||
assert.equal(quotas.balance.remaining, 12.34);
|
||||
assert.equal(quotas.kiloPassBase.remaining, 50);
|
||||
assert.equal(quotas.kiloPassBonus.remaining, 5);
|
||||
assert.equal(quotas.kiloPassUsage.used, 30);
|
||||
assert.equal(quotas.kiloPassRemaining.remaining, 25);
|
||||
});
|
||||
|
||||
test("success: exact $0 balance is valid, not missing", async () => {
|
||||
recordDualFetch({ balance: 0 }, 200, PASS_ACTIVE, 200);
|
||||
const usage = (await getKilocodeUsage("conn-0", baseConnection)) as UsageResult;
|
||||
assert.equal(usage.quotas?.balance.remaining, 0);
|
||||
assert.equal(usage.quotas?.balance.remainingPercentage, 0);
|
||||
assert.equal(typeof usage.message, "undefined");
|
||||
});
|
||||
|
||||
// Case A: pass amounts surface correctly
|
||||
|
||||
test("case A: Base 50, Usage 30, Bonus 5, Remaining 25", async () => {
|
||||
recordDualFetch({ balance: 10 }, 200, PASS_ACTIVE, 200);
|
||||
const usage = (await getKilocodeUsage("conn-a", baseConnection)) as UsageResult;
|
||||
const quotas = usage.quotas ?? {};
|
||||
assert.equal(quotas.kiloPassBase.remaining, 50);
|
||||
assert.equal(quotas.kiloPassBase.total, 50);
|
||||
assert.equal(quotas.kiloPassUsage.used, 30);
|
||||
assert.equal(quotas.kiloPassBonus.remaining, 5);
|
||||
assert.equal(quotas.kiloPassBonus.total, 5);
|
||||
assert.equal(quotas.kiloPassRemaining.remaining, 25);
|
||||
});
|
||||
|
||||
test("Kilo Pass total and remaining preserve distinct usage semantics", () => {
|
||||
const result = buildKiloPassUsageResult({
|
||||
currentPeriodBaseCreditsUsd: 49,
|
||||
currentPeriodBonusCreditsUsd: 24.5,
|
||||
currentPeriodUsageUsd: 47.95,
|
||||
nextBillingAt: null,
|
||||
});
|
||||
|
||||
assert.equal(result.quotas.kiloPassUsage.total, 73.5);
|
||||
assert.equal(result.quotas.kiloPassUsage.used, 47.95);
|
||||
assert.equal(result.quotas.kiloPassRemaining.remaining, 25.55);
|
||||
assert.notEqual(result.quotas.kiloPassUsage.used, result.quotas.kiloPassRemaining.remaining);
|
||||
});
|
||||
|
||||
// Case B: nextBillingAt attached to period quotas
|
||||
|
||||
test("case B: nextBillingAt normalized and attached", async () => {
|
||||
recordDualFetch({ balance: 10 }, 200, PASS_ACTIVE, 200);
|
||||
const usage = (await getKilocodeUsage("conn-b", baseConnection)) as UsageResult;
|
||||
const quotas = usage.quotas ?? {};
|
||||
assert.equal(quotas.kiloPassBase.resetAt, "2026-09-15T00:00:00.000Z");
|
||||
assert.equal(quotas.kiloPassUsage.resetAt, "2026-09-15T00:00:00.000Z");
|
||||
assert.equal(quotas.kiloPassRemaining.resetAt, "2026-09-15T00:00:00.000Z");
|
||||
assert.equal(quotas.kiloPassBonus.resetAt, null);
|
||||
});
|
||||
|
||||
// Case C: balance stays separate from pass quotas
|
||||
|
||||
test("case C: balance not merged into pass remaining", async () => {
|
||||
recordDualFetch({ balance: 11.51 }, 200, PASS_ACTIVE, 200);
|
||||
const usage = (await getKilocodeUsage("conn-c", baseConnection)) as UsageResult;
|
||||
const quotas = usage.quotas ?? {};
|
||||
assert.equal(quotas.balance.remaining, 11.51);
|
||||
assert.equal(quotas.kiloPassRemaining.remaining, 25);
|
||||
});
|
||||
|
||||
// Case D: balance OK, pass fails
|
||||
|
||||
test("case D: pass HTTP 500 leaves balance intact", async () => {
|
||||
recordDualFetch({ balance: 12.34 }, 200, { error: "boom" }, 500);
|
||||
const usage = (await getKilocodeUsage("conn-d", baseConnection)) as UsageResult;
|
||||
assert.equal(usage.plan, "Kilo Code");
|
||||
assert.equal(usage.quotas?.balance.remaining, 12.34);
|
||||
assert.equal(typeof usage.message, "undefined");
|
||||
});
|
||||
|
||||
// Case E: pass OK, balance fails
|
||||
|
||||
test("case E: balance HTTP 500 leaves pass intact", async () => {
|
||||
recordDualFetch({ error: "boom" }, 500, PASS_ACTIVE, 200);
|
||||
const usage = (await getKilocodeUsage("conn-e", baseConnection)) as UsageResult;
|
||||
assert.equal(usage.plan, "Kilo Code");
|
||||
assert.equal(usage.quotas?.kiloPassBase.remaining, 50);
|
||||
assert.equal(typeof usage.message, "undefined");
|
||||
});
|
||||
|
||||
// Case F: no active pass
|
||||
|
||||
test("case F: no subscription leaves balance intact, no pass quotas", async () => {
|
||||
recordDualFetch({ balance: 8.0 }, 200, [{ result: { data: { json: {} } } }], 200);
|
||||
const usage = (await getKilocodeUsage("conn-f", baseConnection)) as UsageResult;
|
||||
assert.equal(usage.quotas?.balance.remaining, 8.0);
|
||||
assert.equal(typeof usage.quotas?.kiloPassBase, "undefined");
|
||||
assert.equal(typeof usage.message, "undefined");
|
||||
});
|
||||
|
||||
// Cases G/H: pass 401 / 429 degrade gracefully
|
||||
|
||||
test("case G: pass 401 leaves balance intact", async () => {
|
||||
recordDualFetch({ balance: 5.0 }, 200, { error: "unauthorized" }, 401);
|
||||
const usage = (await getKilocodeUsage("conn-g", baseConnection)) as UsageResult;
|
||||
assert.equal(usage.quotas?.balance.remaining, 5.0);
|
||||
assert.equal(typeof usage.message, "undefined");
|
||||
});
|
||||
|
||||
test("case H: pass 429 leaves balance intact", async () => {
|
||||
recordDualFetch({ balance: 5.0 }, 200, { error: "limited" }, 429);
|
||||
const usage = (await getKilocodeUsage("conn-h", baseConnection)) as UsageResult;
|
||||
assert.equal(usage.quotas?.balance.remaining, 5.0);
|
||||
assert.equal(typeof usage.message, "undefined");
|
||||
});
|
||||
|
||||
// Case I: invalid pass JSON degrades gracefully
|
||||
|
||||
test("case I: invalid pass JSON leaves balance intact", async () => {
|
||||
globalThis.fetch = async (url: unknown) => {
|
||||
const u = String(url);
|
||||
if (u.includes("/api/profile/balance")) {
|
||||
return jsonResponse({ balance: 5.0 }, 200);
|
||||
}
|
||||
return new Response("not-json", { status: 200 });
|
||||
};
|
||||
const usage = (await getKilocodeUsage("conn-i", baseConnection)) as UsageResult;
|
||||
assert.equal(usage.quotas?.balance.remaining, 5.0);
|
||||
assert.equal(typeof usage.message, "undefined");
|
||||
});
|
||||
|
||||
// Case J: pass response missing required fields
|
||||
|
||||
test("case J: pass missing fields leaves balance intact", async () => {
|
||||
const incomplete = [{ result: { data: { json: { subscription: { tier: "tier_1" } } } } }];
|
||||
recordDualFetch({ balance: 3.0 }, 200, incomplete, 200);
|
||||
const usage = (await getKilocodeUsage("conn-j", baseConnection)) as UsageResult;
|
||||
assert.equal(usage.quotas?.balance.remaining, 3.0);
|
||||
assert.equal(typeof usage.quotas?.kiloPassBase, "undefined");
|
||||
});
|
||||
|
||||
// Case K: null pass response
|
||||
|
||||
test("case K: null pass body leaves balance intact", async () => {
|
||||
recordDualFetch({ balance: 2.0 }, 200, null, 200);
|
||||
const usage = (await getKilocodeUsage("conn-k", baseConnection)) as UsageResult;
|
||||
assert.equal(usage.quotas?.balance.remaining, 2.0);
|
||||
assert.equal(typeof usage.message, "undefined");
|
||||
});
|
||||
|
||||
// Case L: negative or non-finite credits clamped to 0
|
||||
|
||||
test("case L: negative and non-finite credits clamp to 0", () => {
|
||||
const sub: Record<string, unknown> = {
|
||||
status: "active",
|
||||
currentPeriodBaseCreditsUsd: -10,
|
||||
currentPeriodUsageUsd: Number.NaN,
|
||||
currentPeriodBonusCreditsUsd: Number.POSITIVE_INFINITY,
|
||||
};
|
||||
const state = parseKiloPassState({ subscription: sub });
|
||||
assert.notEqual(state, null);
|
||||
assert.equal(state?.currentPeriodBaseCreditsUsd, 0);
|
||||
assert.equal(state?.currentPeriodUsageUsd, 0);
|
||||
assert.equal(state?.currentPeriodBonusCreditsUsd, 0);
|
||||
});
|
||||
|
||||
// Case M: zero amounts are valid
|
||||
|
||||
test("case M: zero base/bonus/usage treated as valid", () => {
|
||||
const sub: Record<string, unknown> = {
|
||||
status: "active",
|
||||
currentPeriodBaseCreditsUsd: 0,
|
||||
currentPeriodUsageUsd: 0,
|
||||
currentPeriodBonusCreditsUsd: 0,
|
||||
};
|
||||
const state = parseKiloPassState({ subscription: sub });
|
||||
assert.notEqual(state, null);
|
||||
const result = buildKiloPassUsageResult(state as NonNullable<typeof state>);
|
||||
assert.equal(result.quotas.kiloPassBase.remaining, 0);
|
||||
assert.equal(result.quotas.kiloPassRemaining.remaining, 0);
|
||||
});
|
||||
|
||||
// parseKiloPassState unit tests
|
||||
|
||||
test("parseKiloPassState: batched tRPC shape", () => {
|
||||
const state = parseKiloPassState(PASS_ACTIVE);
|
||||
assert.notEqual(state, null);
|
||||
assert.equal(state?.currentPeriodBaseCreditsUsd, 50);
|
||||
assert.equal(state?.currentPeriodUsageUsd, 30);
|
||||
assert.equal(state?.currentPeriodBonusCreditsUsd, 5);
|
||||
assert.equal(state?.nextBillingAt, "2026-09-15T00:00:00.000Z");
|
||||
});
|
||||
|
||||
test("parseKiloPassState: plain subscription shape", () => {
|
||||
const sub: Record<string, unknown> = {
|
||||
status: "trialing",
|
||||
currentPeriodBaseCreditsUsd: 19,
|
||||
currentPeriodUsageUsd: 0.01,
|
||||
currentPeriodBonusCreditsUsd: 29.85,
|
||||
nextBillingAt: "2026-07-20T09:30:20.806Z",
|
||||
};
|
||||
const state = parseKiloPassState({ subscription: sub });
|
||||
assert.notEqual(state, null);
|
||||
assert.equal(state?.currentPeriodBaseCreditsUsd, 19);
|
||||
});
|
||||
|
||||
test("parseKiloPassState: canceled and expired status return null", () => {
|
||||
const canceled: Record<string, unknown> = {
|
||||
subscription: {
|
||||
status: "canceled",
|
||||
currentPeriodBaseCreditsUsd: 19,
|
||||
currentPeriodUsageUsd: 0,
|
||||
},
|
||||
};
|
||||
assert.equal(parseKiloPassState(canceled), null);
|
||||
const expired: Record<string, unknown> = {
|
||||
subscription: {
|
||||
status: "expired",
|
||||
currentPeriodBaseCreditsUsd: 19,
|
||||
currentPeriodUsageUsd: 0,
|
||||
},
|
||||
};
|
||||
assert.equal(parseKiloPassState(expired), null);
|
||||
});
|
||||
|
||||
test("parseKiloPassState: past_due status accepted", () => {
|
||||
const sub: Record<string, unknown> = {
|
||||
status: "past_due",
|
||||
currentPeriodBaseCreditsUsd: 19,
|
||||
currentPeriodUsageUsd: 5,
|
||||
};
|
||||
const state = parseKiloPassState({ subscription: sub });
|
||||
assert.notEqual(state, null);
|
||||
assert.equal(state?.currentPeriodBaseCreditsUsd, 19);
|
||||
});
|
||||
|
||||
test("parseKiloPassState: missing amounts return null", () => {
|
||||
assert.equal(parseKiloPassState({ status: "none" }), null);
|
||||
assert.equal(parseKiloPassState({}), null);
|
||||
assert.equal(parseKiloPassState(null), null);
|
||||
});
|
||||
|
||||
test("parseKiloPassState: invalid nextBillingAt becomes null", () => {
|
||||
const sub: Record<string, unknown> = {
|
||||
status: "active",
|
||||
currentPeriodBaseCreditsUsd: 10,
|
||||
currentPeriodUsageUsd: 0,
|
||||
nextBillingAt: "not-a-date",
|
||||
};
|
||||
const state = parseKiloPassState({ subscription: sub });
|
||||
assert.notEqual(state, null);
|
||||
assert.equal(state?.nextBillingAt, null);
|
||||
});
|
||||
|
||||
// Both endpoints fail
|
||||
|
||||
test("both endpoints fail: graceful message", async () => {
|
||||
recordDualFetch({ error: "boom" }, 500, { error: "boom" }, 500);
|
||||
const usage = (await getKilocodeUsage("conn-both-fail", baseConnection)) as UsageResult;
|
||||
assert.equal(usage.plan, "Kilo Code");
|
||||
assert.ok(typeof usage.message === "string");
|
||||
assert.equal(usage.quotas, undefined);
|
||||
});
|
||||
|
||||
// Balance error diagnostics when pass also unavailable
|
||||
|
||||
test("balance 401 with pass down: re-auth message", async () => {
|
||||
recordDualFetch({ error: "unauthorized" }, 401, { error: "unauthorized" }, 401);
|
||||
const usage = (await getKilocodeUsage("conn-401", baseConnection)) as UsageResult;
|
||||
assert.ok(/expired|denied|re-authenticate/i.test(usage.message ?? ""));
|
||||
});
|
||||
|
||||
test("balance 429 with pass down: rate-limit message", async () => {
|
||||
recordDualFetch({ error: "limited" }, 429, { error: "limited" }, 429);
|
||||
const usage = (await getKilocodeUsage("conn-429", baseConnection)) as UsageResult;
|
||||
assert.ok(/rate limit/i.test(usage.message ?? ""));
|
||||
});
|
||||
|
||||
test("network error on both endpoints: graceful message", async () => {
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error("network down");
|
||||
};
|
||||
const usage = (await getKilocodeUsage("conn-net", baseConnection)) as UsageResult;
|
||||
assert.ok(typeof usage.message === "string");
|
||||
});
|
||||
|
||||
test("balance malformed JSON with pass down: message", async () => {
|
||||
globalThis.fetch = async (url: unknown) => {
|
||||
const u = String(url);
|
||||
if (u.includes("/api/profile/balance")) {
|
||||
return new Response("not-json", { status: 200 });
|
||||
}
|
||||
return jsonResponse({}, 200);
|
||||
};
|
||||
const usage = (await getKilocodeUsage("conn-badjson", baseConnection)) as UsageResult;
|
||||
assert.ok(typeof usage.message === "string");
|
||||
});
|
||||
|
||||
test("balance payload without balance field with pass down: message", async () => {
|
||||
recordDualFetch({ credits: 10 }, 200, {}, 200);
|
||||
const usage = (await getKilocodeUsage("conn-nobal", baseConnection)) as UsageResult;
|
||||
assert.ok(typeof usage.message === "string");
|
||||
});
|
||||
|
||||
test("negative balance rejected when pass also down", async () => {
|
||||
recordDualFetch({ balance: -1 }, 200, {}, 200);
|
||||
const usage = (await getKilocodeUsage("conn-neg", baseConnection)) as UsageResult;
|
||||
assert.ok(typeof usage.message === "string");
|
||||
});
|
||||
|
||||
// parseKilocodeBalance unit tests
|
||||
|
||||
test("parseKilocodeBalance: rejects missing/null/negative/non-finite", () => {
|
||||
assert.equal(parseKilocodeBalance(undefined), null);
|
||||
assert.equal(parseKilocodeBalance(null), null);
|
||||
assert.equal(parseKilocodeBalance({}), null);
|
||||
assert.equal(parseKilocodeBalance({ balance: null }), null);
|
||||
assert.equal(parseKilocodeBalance({ balance: -1 }), null);
|
||||
assert.equal(parseKilocodeBalance({ balance: Number.NaN }), null);
|
||||
assert.equal(parseKilocodeBalance({ balance: Number.POSITIVE_INFINITY }), null);
|
||||
});
|
||||
|
||||
test("parseKilocodeBalance: accepts valid values with rounding", () => {
|
||||
assert.equal(parseKilocodeBalance({ balance: 12.345 }), 12.35);
|
||||
assert.equal(parseKilocodeBalance({ balance: 0 }), 0);
|
||||
assert.equal(parseKilocodeBalance({ balance: 12.34 }), 12.34);
|
||||
});
|
||||
|
||||
// buildKilocodeUsageResult unit tests
|
||||
|
||||
test("buildKilocodeUsageResult: exact remaining, USD credits shape", () => {
|
||||
const result = buildKilocodeUsageResult(12.34);
|
||||
assert.equal(result.plan, "Kilo Code");
|
||||
assert.equal(result.quotas.balance.remaining, 12.34);
|
||||
assert.equal(result.quotas.balance.currency, "USD");
|
||||
assert.equal(result.quotas.balance.remainingPercentage, 100);
|
||||
assert.equal(result.quotas.balance.resetAt, null);
|
||||
assert.equal(result.quotas.balance.unlimited, true);
|
||||
});
|
||||
|
||||
// buildKiloPassUsageResult unit tests
|
||||
|
||||
test("buildKiloPassUsageResult: correct quota entries", () => {
|
||||
const result = buildKiloPassUsageResult({
|
||||
currentPeriodBaseCreditsUsd: 50,
|
||||
currentPeriodUsageUsd: 30,
|
||||
currentPeriodBonusCreditsUsd: 5,
|
||||
nextBillingAt: "2026-09-15T00:00:00.000Z",
|
||||
});
|
||||
assert.equal(result.quotas.kiloPassBase.remaining, 50);
|
||||
assert.equal(result.quotas.kiloPassBase.resetAt, "2026-09-15T00:00:00.000Z");
|
||||
assert.equal(result.quotas.kiloPassBonus.remaining, 5);
|
||||
assert.equal(result.quotas.kiloPassUsage.used, 30);
|
||||
assert.equal(result.quotas.kiloPassRemaining.remaining, 25);
|
||||
});
|
||||
|
||||
test("buildKiloPassUsageResult: usage exceeding pool clamps to 0", () => {
|
||||
const result = buildKiloPassUsageResult({
|
||||
currentPeriodBaseCreditsUsd: 10,
|
||||
currentPeriodUsageUsd: 20,
|
||||
currentPeriodBonusCreditsUsd: 5,
|
||||
nextBillingAt: null,
|
||||
});
|
||||
assert.equal(result.quotas.kiloPassRemaining.remaining, 0);
|
||||
});
|
||||
Reference in New Issue
Block a user