mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-07-09 17:18:24 +00:00
Add Codex manual reset account metrics
This commit is contained in:
parent
15fcaaecf5
commit
b6f8bb3c52
11 changed files with 389 additions and 34 deletions
|
|
@ -39,21 +39,124 @@ const codexAccountRateLimitMapping: ProviderAccountMappingConfig = {
|
|||
kind: "quota",
|
||||
label: "Primary quota",
|
||||
limit: 100,
|
||||
remaining: "100 - $.rate_limit.primary_window.used_percent",
|
||||
resetAt: "$.rate_limit.primary_window.reset_at",
|
||||
remaining: [
|
||||
"100 - $.rate_limit.primary_window.used_percent",
|
||||
"100 - $.rate_limits.primary.used_percent"
|
||||
],
|
||||
resetAt: [
|
||||
"$.rate_limit.primary_window.reset_at",
|
||||
"$.rate_limit.primary_window.resets_at",
|
||||
"$.rate_limits.primary.resets_at"
|
||||
],
|
||||
unit: "%",
|
||||
used: "$.rate_limit.primary_window.used_percent",
|
||||
used: [
|
||||
"$.rate_limit.primary_window.used_percent",
|
||||
"$.rate_limits.primary.used_percent"
|
||||
],
|
||||
window: "primary"
|
||||
},
|
||||
{
|
||||
id: "codex_manual_resets",
|
||||
kind: "requests",
|
||||
label: "Manual resets",
|
||||
remaining: [
|
||||
"$.resetsAvailable",
|
||||
"$.availableRateLimitResetCount",
|
||||
"$.rate_limit.resetsAvailable",
|
||||
"$.rate_limits.resetsAvailable",
|
||||
"$.rate_limit.manual_resets.remaining",
|
||||
"$.rate_limit.manual_resets.resetsAvailable",
|
||||
"$.rate_limit.manual_reset.remaining",
|
||||
"$.rate_limit.manual_reset.resetsAvailable",
|
||||
"$.rate_limits.manual_resets.remaining",
|
||||
"$.rate_limits.manual_resets.resetsAvailable",
|
||||
"$.rate_limits.manual_reset.remaining",
|
||||
"$.rate_limits.manual_reset.resetsAvailable",
|
||||
"$.manual_resets.remaining",
|
||||
"$.manual_resets.resetsAvailable",
|
||||
"$.manual_reset.remaining",
|
||||
"$.manual_reset.resetsAvailable",
|
||||
"$.resets.remaining",
|
||||
"$.resets.resetsAvailable",
|
||||
"$.rate_limit.manual_resets.available",
|
||||
"$.rate_limit.manual_reset.available",
|
||||
"$.rate_limit.resets.available",
|
||||
"$.rate_limits.resets.available",
|
||||
"$.manual_resets.available",
|
||||
"$.manual_reset.available",
|
||||
"$.resets.available"
|
||||
],
|
||||
resetAt: [
|
||||
"$.resetExpires",
|
||||
"$.expires_at",
|
||||
"$.resets_at",
|
||||
"$.rate_limit.manual_resets.expires_at",
|
||||
"$.rate_limit.manual_resets.expire_at",
|
||||
"$.rate_limit.manual_resets.reset_at",
|
||||
"$.rate_limit.manual_resets.resets_at",
|
||||
"$.rate_limit.manual_reset.expires_at",
|
||||
"$.rate_limit.manual_reset.expire_at",
|
||||
"$.rate_limit.manual_reset.reset_at",
|
||||
"$.rate_limit.manual_reset.resets_at",
|
||||
"$.rate_limits.manual_resets.expires_at",
|
||||
"$.rate_limits.manual_resets.expire_at",
|
||||
"$.rate_limits.manual_resets.reset_at",
|
||||
"$.rate_limits.manual_resets.resets_at",
|
||||
"$.rate_limits.manual_reset.expires_at",
|
||||
"$.rate_limits.manual_reset.expire_at",
|
||||
"$.rate_limits.manual_reset.reset_at",
|
||||
"$.rate_limits.manual_reset.resets_at",
|
||||
"$.rate_limit.resets.expires_at",
|
||||
"$.rate_limit.resets.expire_at",
|
||||
"$.rate_limit.resets.reset_at",
|
||||
"$.rate_limit.resets.resets_at",
|
||||
"$.rate_limits.resets.expires_at",
|
||||
"$.rate_limits.resets.expire_at",
|
||||
"$.rate_limits.resets.reset_at",
|
||||
"$.rate_limits.resets.resets_at",
|
||||
"$.manual_resets.expires_at",
|
||||
"$.manual_resets.expire_at",
|
||||
"$.manual_resets.reset_at",
|
||||
"$.manual_resets.resets_at",
|
||||
"$.manual_reset.expires_at",
|
||||
"$.manual_reset.expire_at",
|
||||
"$.manual_reset.reset_at",
|
||||
"$.manual_reset.resets_at",
|
||||
"$.resets.expires_at",
|
||||
"$.resets.expire_at",
|
||||
"$.resets.reset_at",
|
||||
"$.resets.resets_at"
|
||||
],
|
||||
unit: "resets",
|
||||
used: [
|
||||
"$.rate_limit.manual_resets.used",
|
||||
"$.rate_limit.manual_reset.used",
|
||||
"$.rate_limits.manual_resets.used",
|
||||
"$.manual_resets.used",
|
||||
"$.manual_reset.used",
|
||||
"$.resets.used"
|
||||
],
|
||||
window: "manual-reset"
|
||||
},
|
||||
{
|
||||
id: "codex_secondary_quota",
|
||||
kind: "quota",
|
||||
label: "Secondary quota",
|
||||
limit: 100,
|
||||
remaining: "100 - $.rate_limit.secondary_window.used_percent",
|
||||
resetAt: "$.rate_limit.secondary_window.reset_at",
|
||||
remaining: [
|
||||
"100 - $.rate_limit.secondary_window.used_percent",
|
||||
"100 - $.rate_limits.secondary.used_percent"
|
||||
],
|
||||
resetAt: [
|
||||
"$.rate_limit.secondary_window.reset_at",
|
||||
"$.rate_limit.secondary_window.resets_at",
|
||||
"$.rate_limits.secondary.resets_at"
|
||||
],
|
||||
unit: "%",
|
||||
used: "$.rate_limit.secondary_window.used_percent",
|
||||
used: [
|
||||
"$.rate_limit.secondary_window.used_percent",
|
||||
"$.rate_limits.secondary.used_percent"
|
||||
],
|
||||
window: "secondary"
|
||||
},
|
||||
{
|
||||
|
|
@ -164,7 +267,7 @@ export function readCodexAuth(): OAuthTokenSet | undefined {
|
|||
};
|
||||
}
|
||||
|
||||
function codexProviderAccountConfig(): ProviderAccountConfig {
|
||||
export function codexProviderAccountConfig(): ProviderAccountConfig {
|
||||
return {
|
||||
connectors: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import type {
|
|||
ProviderAccountLocalEstimateConnectorConfig,
|
||||
ProviderAccountLocalWindowConfig,
|
||||
ProviderAccountMappedMeterConfig,
|
||||
ProviderAccountMappedNumberExpression,
|
||||
ProviderAccountMappedStringExpression,
|
||||
ProviderAccountMeter,
|
||||
ProviderAccountMeterKind,
|
||||
ProviderAccountMeterUnit,
|
||||
|
|
@ -1345,7 +1347,16 @@ type JsonPathFilterCondition = {
|
|||
path: string[];
|
||||
};
|
||||
|
||||
function readMappedNumber(value: number | string | undefined, payload: unknown): number | undefined {
|
||||
function readMappedNumber(value: ProviderAccountMappedNumberExpression | undefined, payload: unknown): number | undefined {
|
||||
if (Array.isArray(value)) {
|
||||
for (const candidate of value) {
|
||||
const resolved = readMappedNumber(candidate, payload);
|
||||
if (resolved !== undefined) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === "number") {
|
||||
return normalizeNumber(value);
|
||||
}
|
||||
|
|
@ -1382,7 +1393,16 @@ function resolveMappedNumberTerm(term: string, payload: unknown): unknown {
|
|||
return trimmed.startsWith("$") ? readJsonPath(payload, trimmed) : trimmed;
|
||||
}
|
||||
|
||||
function readMappedString(value: string | undefined, payload: unknown): string | undefined {
|
||||
function readMappedString(value: ProviderAccountMappedStringExpression | undefined, payload: unknown): string | undefined {
|
||||
if (Array.isArray(value)) {
|
||||
for (const candidate of value) {
|
||||
const resolved = readMappedString(candidate, payload);
|
||||
if (resolved !== undefined) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -1390,7 +1410,16 @@ function readMappedString(value: string | undefined, payload: unknown): string |
|
|||
return readString(resolved);
|
||||
}
|
||||
|
||||
function readMappedDateString(value: string | undefined, payload: unknown): string | undefined {
|
||||
function readMappedDateString(value: ProviderAccountMappedStringExpression | undefined, payload: unknown): string | undefined {
|
||||
if (Array.isArray(value)) {
|
||||
for (const candidate of value) {
|
||||
const resolved = readMappedDateString(candidate, payload);
|
||||
if (resolved !== undefined) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
metricToneBar, metricToneStroke, motion, normalizeAgentFilterValue, normalizeOverviewWidget, normalizeOverviewWidgets,
|
||||
OverviewMetricKind, overviewMetricOptions, overviewWidgetCollisionDetection, OverviewWidgetConfig, OverviewWidgetSize, overviewWidgetSizeOptions,
|
||||
OverviewWidgetType, OverviewWidgetVariant, Pencil, Pie, PieChart, Plus,
|
||||
PointerSensor, primaryProviderAccountMeter, providerAccountMeterProgress, providerAccountMetersForDisplay, providerAccountProgressClass,
|
||||
PointerSensor, primaryProviderAccountMeter, providerAccountMeterProgress, providerAccountMetersForDisplay, providerAccountProgressClass, isProviderAccountManualResetMeter,
|
||||
providerAccountSnapshotKey, providerAccountSnapshotLabel,
|
||||
ProviderAccountMeter, ProviderAccountSnapshot, ReactNode, ReactPointerEvent, rectSortingStrategy, RefreshCw, Select,
|
||||
SelectControl, SortableContext, sortableKeyboardCoordinates, systemStatusIconClass, systemStatusPointTooltip, systemStatusSegmentClass,
|
||||
|
|
@ -2432,7 +2432,15 @@ function primaryProviderAccountBalanceMeter(account: ProviderAccountSnapshot): P
|
|||
}
|
||||
|
||||
function providerAccountMetersForDisplayOrdered(account: ProviderAccountSnapshot, maxCount: number): ProviderAccountMeter[] {
|
||||
const ordered = [...providerAccountQuotaMeters(account), ...providerAccountBalanceMeters(account)];
|
||||
const quotaMeters = providerAccountQuotaMeters(account);
|
||||
const manualResetMeters = account.meters.filter(isProviderAccountManualResetMeter);
|
||||
const leadingQuotaCount = manualResetMeters.length > 0 ? Math.min(quotaMeters.length, maxCount <= 2 ? 1 : 2) : quotaMeters.length;
|
||||
const ordered = [
|
||||
...quotaMeters.slice(0, leadingQuotaCount),
|
||||
...manualResetMeters,
|
||||
...providerAccountBalanceMeters(account),
|
||||
...quotaMeters.slice(leadingQuotaCount)
|
||||
];
|
||||
const seen = new Set<string>();
|
||||
const unique = ordered.filter((meter) => {
|
||||
const key = `${meter.id}:${meter.kind}:${meter.window ?? ""}`;
|
||||
|
|
@ -2470,10 +2478,11 @@ function compareProviderAccountQuotaMeters(a: ProviderAccountMeter, b: ProviderA
|
|||
}
|
||||
|
||||
function providerAccountMeterWindowRank(meter: ProviderAccountMeter): number {
|
||||
if (meter.window === "5h" || meter.id.toLowerCase().includes("5h") || meter.label.toLowerCase().includes("5h")) {
|
||||
const text = `${meter.window ?? ""} ${meter.id} ${meter.label}`.toLowerCase();
|
||||
if (meter.window === "5h" || text.includes("5h") || text.includes("primary")) {
|
||||
return 0;
|
||||
}
|
||||
if (meter.window === "weekly" || meter.id.toLowerCase().includes("weekly") || meter.label.toLowerCase().includes("weekly")) {
|
||||
if (meter.window === "weekly" || text.includes("weekly") || text.includes("secondary")) {
|
||||
return 1;
|
||||
}
|
||||
if (meter.window === "daily") return 2;
|
||||
|
|
|
|||
|
|
@ -1357,12 +1357,21 @@ export const appCopy: Record<ResolvedLanguage, AppCopy> = {
|
|||
"Wrapper plugin": "包装插件",
|
||||
"5h quota": "5 小时额度",
|
||||
"Granted balance": "赠送余额",
|
||||
"Individual limit": "个人额度",
|
||||
"Lifetime tokens": "累计令牌",
|
||||
"Manual resets": "主动重置次数",
|
||||
"Monthly budget": "月度预算",
|
||||
"Peak daily tokens": "日峰值令牌",
|
||||
"Primary quota": "主额度",
|
||||
"Secondary quota": "副额度",
|
||||
"Topped-up balance": "充值余额",
|
||||
"Total credits": "总额度",
|
||||
"Total usage": "总用量",
|
||||
"Voucher balance": "代金券余额",
|
||||
"Weekly quota": "周额度",
|
||||
"expired": "已过期",
|
||||
"expires in": "剩余",
|
||||
"resets": "次",
|
||||
"All": "全部",
|
||||
"API endpoint": "API 地址",
|
||||
"Available": "可用",
|
||||
|
|
|
|||
|
|
@ -410,7 +410,12 @@ export function primaryProviderAccountMeter(account: ProviderAccountSnapshot): P
|
|||
}
|
||||
|
||||
export function providerAccountMetersForDisplay(account: ProviderAccountSnapshot, maxCount: number): ProviderAccountMeter[] {
|
||||
return account.meters.slice(0, maxCount);
|
||||
const meters = account.meters.slice(0, maxCount);
|
||||
const manualResetMeter = account.meters.find(isProviderAccountManualResetMeter);
|
||||
if (!manualResetMeter || meters.includes(manualResetMeter) || meters.length < maxCount) {
|
||||
return meters;
|
||||
}
|
||||
return [...meters.slice(0, Math.max(0, maxCount - 1)), manualResetMeter];
|
||||
}
|
||||
|
||||
export function providerAccountMeterRemainingRatio(meter: ProviderAccountMeter): number | undefined {
|
||||
|
|
@ -483,26 +488,32 @@ export function formatProviderAccountNumber(value: number): string {
|
|||
return new Intl.NumberFormat(undefined, { maximumFractionDigits: 6 }).format(value);
|
||||
}
|
||||
|
||||
export function formatProviderAccountReset(value: string): string {
|
||||
export function formatProviderAccountReset(value: string, translate: (value: string) => string = (item) => item): string {
|
||||
const timestamp = new Date(value).getTime();
|
||||
if (!Number.isFinite(timestamp)) {
|
||||
return value;
|
||||
}
|
||||
const minutes = Math.round((timestamp - Date.now()) / 60000);
|
||||
if (minutes <= 0) {
|
||||
return "soon";
|
||||
return translate("expired");
|
||||
}
|
||||
const prefix = translate("expires in");
|
||||
if (minutes < 60) {
|
||||
return `${minutes}m`;
|
||||
return `${prefix} ${minutes}m`;
|
||||
}
|
||||
const hours = Math.round(minutes / 60);
|
||||
if (hours < 48) {
|
||||
return `${hours}h`;
|
||||
return `${prefix} ${hours}h`;
|
||||
}
|
||||
return `${Math.round(hours / 24)}d`;
|
||||
return `${prefix} ${Math.round(hours / 24)}d`;
|
||||
}
|
||||
|
||||
export function formatProviderAccountMeterTitle(meter: ProviderAccountMeter, translate: (value: string) => string): string {
|
||||
const label = translate(meter.label);
|
||||
return meter.resetAt ? `${label} (${formatProviderAccountReset(meter.resetAt)})` : label;
|
||||
return meter.resetAt ? `${label} (${formatProviderAccountReset(meter.resetAt, translate)})` : label;
|
||||
}
|
||||
|
||||
export function isProviderAccountManualResetMeter(meter: ProviderAccountMeter): boolean {
|
||||
const text = `${meter.id} ${meter.label} ${meter.window ?? ""}`.toLowerCase();
|
||||
return text.includes("manual_reset") || text.includes("manual reset") || text.includes("manual-reset");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1286,7 +1286,7 @@ export function createProviderAccountDraftFromConfig(account: ProviderAccountCon
|
|||
usageStatusPath: httpJsonConnector.mapping.status ?? "",
|
||||
usageSubscriptionLimitPath: stringValue(subscriptionMeter?.limit) || "",
|
||||
usageSubscriptionRemainingPath: stringValue(subscriptionMeter?.remaining) || "",
|
||||
usageSubscriptionResetPath: subscriptionMeter?.resetAt ?? "",
|
||||
usageSubscriptionResetPath: stringValue(subscriptionMeter?.resetAt) || "",
|
||||
usageSubscriptionUnit: stringValue(subscriptionMeter?.unit) || "tokens"
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,9 +71,12 @@ export const trayText: Record<ResolvedLanguage, Record<string, string>> = {
|
|||
"F": "五",
|
||||
"Granted balance": "赠送余额",
|
||||
"Input": "输入",
|
||||
"Individual limit": "个人额度",
|
||||
"Less": "少",
|
||||
"Lifetime tokens": "累计令牌",
|
||||
"Longest streak": "最长连续",
|
||||
"M": "一",
|
||||
"Manual resets": "主动重置次数",
|
||||
"Monthly budget": "月度预算",
|
||||
"Model Share": "模型占比",
|
||||
"More": "多",
|
||||
|
|
@ -84,8 +87,11 @@ export const trayText: Record<ResolvedLanguage, Record<string, string>> = {
|
|||
"Output": "输出",
|
||||
"Overview": "概览",
|
||||
"Open CCR": "打开 CCR",
|
||||
"Peak daily tokens": "日峰值令牌",
|
||||
"Primary quota": "主额度",
|
||||
"Quit": "退出",
|
||||
"Refresh": "刷新",
|
||||
"Secondary quota": "副额度",
|
||||
"Subscription": "订阅",
|
||||
"Success": "成功",
|
||||
"Success rate": "成功率",
|
||||
|
|
@ -112,10 +118,13 @@ export const trayText: Record<ResolvedLanguage, Record<string, string>> = {
|
|||
"day": "天",
|
||||
"days": "天",
|
||||
"error": "错误",
|
||||
"expired": "已过期",
|
||||
"expires in": "剩余",
|
||||
"hours": "小时",
|
||||
"minutes": "分钟",
|
||||
"ok": "正常",
|
||||
"requests": "请求",
|
||||
"resets": "次",
|
||||
"soon": "即将",
|
||||
"tokens": "令牌",
|
||||
"unsupported": "不支持",
|
||||
|
|
@ -652,7 +661,17 @@ export function accountStatusRank(status: ProviderAccountSnapshot["status"]): nu
|
|||
}
|
||||
|
||||
export function accountMetersForDisplay(snapshot: ProviderAccountSnapshot, maxCount: number): ProviderAccountMeter[] {
|
||||
return snapshot.meters.slice(0, maxCount);
|
||||
const meters = snapshot.meters.slice(0, maxCount);
|
||||
const manualResetMeter = snapshot.meters.find(isAccountManualResetMeter);
|
||||
if (!manualResetMeter || meters.includes(manualResetMeter) || meters.length < maxCount) {
|
||||
return meters;
|
||||
}
|
||||
return [...meters.slice(0, Math.max(0, maxCount - 1)), manualResetMeter];
|
||||
}
|
||||
|
||||
function isAccountManualResetMeter(meter: ProviderAccountMeter): boolean {
|
||||
const text = `${meter.id} ${meter.label} ${meter.window ?? ""}`.toLowerCase();
|
||||
return text.includes("manual_reset") || text.includes("manual reset") || text.includes("manual-reset");
|
||||
}
|
||||
|
||||
export function meterRemainingRatio(meter: ProviderAccountMeter): number | undefined {
|
||||
|
|
@ -706,28 +725,29 @@ export function formatMeterNumber(value: number): string {
|
|||
return new Intl.NumberFormat(undefined, { maximumFractionDigits: 6 }).format(value);
|
||||
}
|
||||
|
||||
export function formatAccountReset(value: string): string {
|
||||
export function formatAccountReset(value: string, translate: (value: string) => string = (item) => item): string {
|
||||
const timestamp = new Date(value).getTime();
|
||||
if (!Number.isFinite(timestamp)) {
|
||||
return value;
|
||||
}
|
||||
const minutes = Math.round((timestamp - Date.now()) / 60000);
|
||||
if (minutes <= 0) {
|
||||
return "soon";
|
||||
return translate("expired");
|
||||
}
|
||||
const prefix = translate("expires in");
|
||||
if (minutes < 60) {
|
||||
return `${minutes}m`;
|
||||
return `${prefix} ${minutes}m`;
|
||||
}
|
||||
const hours = Math.round(minutes / 60);
|
||||
if (hours < 48) {
|
||||
return `${hours}h`;
|
||||
return `${prefix} ${hours}h`;
|
||||
}
|
||||
return `${Math.round(hours / 24)}d`;
|
||||
return `${prefix} ${Math.round(hours / 24)}d`;
|
||||
}
|
||||
|
||||
export function formatAccountMeterTitle(meter: ProviderAccountMeter, translate: (value: string) => string): string {
|
||||
const label = translateAccountMeterLabel(meter.label, translate);
|
||||
return meter.resetAt ? `${label} (${formatAccountReset(meter.resetAt)})` : label;
|
||||
return meter.resetAt ? `${label} (${formatAccountReset(meter.resetAt, translate)})` : label;
|
||||
}
|
||||
|
||||
export function accountStatusClass(status: ProviderAccountSnapshot["status"]): string {
|
||||
|
|
|
|||
|
|
@ -223,15 +223,18 @@ export type ProviderAccountMappingConfig = {
|
|||
status?: string;
|
||||
};
|
||||
|
||||
export type ProviderAccountMappedNumberExpression = number | string | Array<number | string>;
|
||||
export type ProviderAccountMappedStringExpression = string | string[];
|
||||
|
||||
export type ProviderAccountMappedMeterConfig = {
|
||||
id: string;
|
||||
kind?: ProviderAccountMeterKind;
|
||||
label: string;
|
||||
limit?: number | string;
|
||||
remaining?: number | string;
|
||||
resetAt?: string;
|
||||
limit?: ProviderAccountMappedNumberExpression;
|
||||
remaining?: ProviderAccountMappedNumberExpression;
|
||||
resetAt?: ProviderAccountMappedStringExpression;
|
||||
unit?: ProviderAccountMeterUnit;
|
||||
used?: number | string;
|
||||
used?: ProviderAccountMappedNumberExpression;
|
||||
window?: ProviderAccountMeterWindow;
|
||||
};
|
||||
|
||||
|
|
|
|||
41
tests/main/local-agent-provider-codex.test.mjs
Normal file
41
tests/main/local-agent-provider-codex.test.mjs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { codexProviderAccountConfig } from "../../src/main/local-agent-providers/codex.ts";
|
||||
|
||||
test("Codex provider account config includes manual reset meter", () => {
|
||||
const config = codexProviderAccountConfig();
|
||||
const usageConnector = config.connectors.find((connector) => connector.type === "http-json" && connector.endpoint.endsWith("/wham/usage"));
|
||||
assert.ok(usageConnector);
|
||||
|
||||
const meters = usageConnector.mapping.meters;
|
||||
const manualResetMeter = meters.find((meter) => meter.id === "codex_manual_resets");
|
||||
assert.ok(manualResetMeter);
|
||||
assert.equal(manualResetMeter.label, "Manual resets");
|
||||
assert.equal(manualResetMeter.unit, "resets");
|
||||
assert.equal(manualResetMeter.window, "manual-reset");
|
||||
assert.ok(Array.isArray(manualResetMeter.remaining));
|
||||
assert.ok(manualResetMeter.remaining.includes("$.resetsAvailable"));
|
||||
assert.ok(manualResetMeter.remaining.includes("$.availableRateLimitResetCount"));
|
||||
assert.ok(manualResetMeter.remaining.includes("$.rate_limit.manual_resets.remaining"));
|
||||
assert.ok(Array.isArray(manualResetMeter.resetAt));
|
||||
assert.ok(manualResetMeter.resetAt.includes("$.resetExpires"));
|
||||
assert.ok(manualResetMeter.resetAt.includes("$.expires_at"));
|
||||
assert.ok(manualResetMeter.resetAt.includes("$.rate_limit.manual_resets.expires_at"));
|
||||
assert.ok(manualResetMeter.resetAt.includes("$.manual_resets.reset_at"));
|
||||
});
|
||||
|
||||
test("Codex quota meters accept reset_at and resets_at usage shapes", () => {
|
||||
const config = codexProviderAccountConfig();
|
||||
const usageConnector = config.connectors.find((connector) => connector.type === "http-json" && connector.endpoint.endsWith("/wham/usage"));
|
||||
const primaryQuota = usageConnector.mapping.meters.find((meter) => meter.id === "codex_primary_quota");
|
||||
const secondaryQuota = usageConnector.mapping.meters.find((meter) => meter.id === "codex_secondary_quota");
|
||||
|
||||
assert.ok(Array.isArray(primaryQuota.resetAt));
|
||||
assert.ok(primaryQuota.resetAt.includes("$.rate_limit.primary_window.reset_at"));
|
||||
assert.ok(primaryQuota.resetAt.includes("$.rate_limit.primary_window.resets_at"));
|
||||
assert.ok(primaryQuota.resetAt.includes("$.rate_limits.primary.resets_at"));
|
||||
assert.ok(Array.isArray(secondaryQuota.resetAt));
|
||||
assert.ok(secondaryQuota.resetAt.includes("$.rate_limit.secondary_window.reset_at"));
|
||||
assert.ok(secondaryQuota.resetAt.includes("$.rate_limit.secondary_window.resets_at"));
|
||||
assert.ok(secondaryQuota.resetAt.includes("$.rate_limits.secondary.resets_at"));
|
||||
});
|
||||
|
|
@ -3,7 +3,7 @@ import test from "node:test";
|
|||
import * as React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { OverviewView } from "../../src/renderer/pages/home/components/dashboard.tsx";
|
||||
import type { OverviewWidgetConfig } from "../../src/shared/app.ts";
|
||||
import type { OverviewWidgetConfig, ProviderAccountSnapshot } from "../../src/shared/app.ts";
|
||||
import { accountSnapshots, installBrowserGlobals, usageStats } from "./fixtures.ts";
|
||||
|
||||
installBrowserGlobals();
|
||||
|
|
@ -82,3 +82,80 @@ test("OverviewView renders the empty widget layout state", () => {
|
|||
assert.match(html, /No widgets configured/);
|
||||
assert.match(html, /aria-label="Edit widgets"/);
|
||||
});
|
||||
|
||||
test("OverviewView prioritizes Codex manual resets before folded balance meters", () => {
|
||||
const resetAt = new Date(Date.now() + 72 * 60 * 60 * 1000).toISOString();
|
||||
const codexAccount: ProviderAccountSnapshot = {
|
||||
meters: [
|
||||
{
|
||||
id: "codex_primary_quota",
|
||||
kind: "quota",
|
||||
label: "Primary quota",
|
||||
limit: 100,
|
||||
remaining: 96,
|
||||
resetAt,
|
||||
unit: "%",
|
||||
window: "primary"
|
||||
},
|
||||
{
|
||||
id: "codex_secondary_quota",
|
||||
kind: "quota",
|
||||
label: "Secondary quota",
|
||||
limit: 100,
|
||||
remaining: 68,
|
||||
resetAt,
|
||||
unit: "%",
|
||||
window: "secondary"
|
||||
},
|
||||
{
|
||||
id: "codex_individual_limit",
|
||||
kind: "quota",
|
||||
label: "Individual limit",
|
||||
limit: 100,
|
||||
remaining: 42,
|
||||
resetAt,
|
||||
unit: "credits",
|
||||
window: "monthly"
|
||||
},
|
||||
{
|
||||
id: "codex_credit_balance",
|
||||
kind: "balance",
|
||||
label: "Credit balance",
|
||||
remaining: 0,
|
||||
unit: "credits"
|
||||
},
|
||||
{
|
||||
id: "codex_manual_resets",
|
||||
kind: "requests",
|
||||
label: "Manual resets",
|
||||
remaining: 2,
|
||||
resetAt,
|
||||
unit: "resets",
|
||||
window: "manual-reset"
|
||||
}
|
||||
],
|
||||
provider: "Codex API",
|
||||
source: "http-json",
|
||||
status: "ok",
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
<OverviewView
|
||||
overviewWidgets={[{ enabled: true, id: "account", size: "4:2", type: "account-balance", variant: "cards" }]}
|
||||
providerAccounts={[codexAccount]}
|
||||
refreshProviderAccounts={() => undefined}
|
||||
setUsageRange={() => undefined}
|
||||
usageRange="30d"
|
||||
usageStats={usageStats("30d")}
|
||||
onWidgetsChange={() => undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
assert.match(html, /Primary quota/);
|
||||
assert.match(html, /Secondary quota/);
|
||||
assert.match(html, /Manual resets/);
|
||||
assert.match(html, /expires in/);
|
||||
assert.match(html, /2 resets/);
|
||||
assert.doesNotMatch(html, /Credit balance/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -144,6 +144,59 @@ test("AccountSummaryPanel covers empty and metered account states", () => {
|
|||
assert.match(meteredHtml, /style="\s*width:42%"/);
|
||||
});
|
||||
|
||||
test("AccountSummaryPanel prioritizes Codex manual reset meter with expiration", () => {
|
||||
const resetAt = new Date(Date.now() + 72 * 60 * 60 * 1000).toISOString();
|
||||
const html = renderToStaticMarkup(
|
||||
<AccountSummaryPanel
|
||||
snapshots={[
|
||||
{
|
||||
meters: [
|
||||
{
|
||||
id: "codex_primary_quota",
|
||||
kind: "quota",
|
||||
label: "Primary quota",
|
||||
limit: 100,
|
||||
remaining: 66,
|
||||
resetAt,
|
||||
unit: "%",
|
||||
window: "primary"
|
||||
},
|
||||
{
|
||||
id: "codex_secondary_quota",
|
||||
kind: "quota",
|
||||
label: "Secondary quota",
|
||||
limit: 100,
|
||||
remaining: 80,
|
||||
unit: "%",
|
||||
window: "secondary"
|
||||
},
|
||||
{
|
||||
id: "codex_manual_resets",
|
||||
kind: "requests",
|
||||
label: "Manual resets",
|
||||
remaining: 2,
|
||||
resetAt,
|
||||
unit: "resets",
|
||||
window: "manual-reset"
|
||||
}
|
||||
],
|
||||
provider: "Codex API",
|
||||
source: "http-json",
|
||||
status: "ok",
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
]}
|
||||
variant="bar"
|
||||
/>
|
||||
);
|
||||
|
||||
assert.match(html, /Primary quota/);
|
||||
assert.match(html, /Manual resets/);
|
||||
assert.match(html, /expires in/);
|
||||
assert.match(html, /2 resets/);
|
||||
assert.doesNotMatch(html, /Secondary quota/);
|
||||
});
|
||||
|
||||
test("RangeSwitch renders every usage range option", () => {
|
||||
const html = renderToStaticMarkup(<RangeSwitch range="7d" onChange={() => undefined} />);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue