Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 244 additions & 0 deletions apps/web/src/components/chat/ThreadDetailsUsage.logic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import {
ProviderDriverKind,
ProviderInstanceId,
type ServerProvider,
type ServerProviderUsageLimits,
type UsageLimitSourceSnapshot,
UsageLimitSourceId,
} from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { nextPoolReset, poolSources, resolveThreadDetailsLimits } from "./ThreadDetailsUsage.logic";

const now = Date.parse("2026-10-30T12:00:00.000Z");
const checkedAt = "2026-10-30T11:59:00.000Z";
const claude = ProviderDriverKind.make("claudeAgent");
const codex = ProviderDriverKind.make("codex");
const instanceId = ProviderInstanceId.make("claudeAgent");

function limitsAt(usedPercent: number, at = checkedAt): ServerProviderUsageLimits {
return {
checkedAt: at,
windows: [
{
id: "five_hour",
kind: "session",
label: "Session",
usedPercent,
windowDurationMins: 300,
resetsAt: "2026-10-30T13:17:00.000Z",
},
],
};
}

function limitsResettingAt(usedPercent: number, resetsAt: string): ServerProviderUsageLimits {
const [window] = limitsAt(usedPercent).windows;
return { checkedAt, windows: [{ ...window!, resetsAt }] };
}

const unsupported: ServerProviderUsageLimits = {
checkedAt,
windows: [],
unavailable: { reason: "unsupported" },
};

function claudeInstance(usageLimits: ServerProviderUsageLimits): ServerProvider {
return {
instanceId,
driver: claude,
enabled: true,
installed: true,
version: null,
status: "ready",
auth: { status: "authenticated", type: "customEndpoint", label: "Custom API endpoint" },
checkedAt,
models: [],
slashCommands: [],
skills: [],
usageLimits,
};
}

function signedIn(id: string, email: string, usageLimits: ServerProviderUsageLimits) {
const base = claudeInstance(usageLimits);
return {
...base,
instanceId: ProviderInstanceId.make(id),
auth: { ...base.auth, email },
} satisfies ServerProvider;
}

interface HubAccountInput {
readonly id: string;
readonly email?: string;
readonly driver?: ProviderDriverKind;
readonly limits: ServerProviderUsageLimits;
}

function hub(
label: string,
accounts: readonly HubAccountInput[],
error?: string,
): UsageLimitSourceSnapshot {
return {
id: UsageLimitSourceId.make(label),
kind: "cliproxy",
label,
checkedAt,
accounts: accounts.map((account) => ({
id: account.id,
driver: account.driver ?? claude,
...(account.email ? { email: account.email } : {}),
usageLimits: account.limits,
})),
...(error ? { error } : {}),
};
}

/** What the card shows, reduced to the rows a user reads. */
function shown(
own: ServerProviderUsageLimits,
sources: readonly UsageLimitSourceSnapshot[],
natives: readonly ServerProvider[] = [],
) {
const resolved = resolveThreadDetailsLimits(
instanceId,
[claudeInstance(own), ...natives],
sources,
now,
);
if (resolved?.kind !== "pool") return resolved;
return {
kind: resolved.kind,
rows: resolved.windows.map((window) => ({
label: window.label,
left: window.remainingPercent,
accounts: window.members.length,
from: poolSources(window),
})),
};
}

describe("resolveThreadDetailsLimits", () => {
it("shows nothing without a selected, known instance, or before its first reading", () => {
expect(resolveThreadDetailsLimits(null, [claudeInstance(unsupported)], [], now)).toBeNull();
expect(
resolveThreadDetailsLimits(ProviderInstanceId.make("gone"), [], [hub("Hub", [])], now),
).toBeNull();
const { usageLimits: _unread, ...unread } = claudeInstance(unsupported);
const readableHub = hub("Hub", [{ id: "a.json", limits: limitsAt(20) }]);
expect(resolveThreadDetailsLimits(instanceId, [unread], [readableHub], now)).toBeNull();
});

it("keeps an instance's own readable limits over any hub", () => {
const own = limitsAt(40);
expect(shown(own, [hub("Hub", [{ id: "a.json", limits: limitsAt(90) }])])).toEqual({
kind: "instance",
limits: own,
});
});

it.each([
{
name: "one hub account",
sources: [hub("Hub", [{ id: "a.json", email: "a@example.com", limits: limitsAt(19) }])],
rows: [{ label: "Session", left: 81, accounts: 1, from: ["Hub"] }],
},
{
name: "several accounts, pooled like Usage → Limits",
sources: [
hub("Hub", [
{ id: "a.json", email: "a@example.com", limits: limitsAt(20) },
{ id: "b.json", email: "b@example.com", limits: limitsAt(60) },
]),
],
rows: [{ label: "Session", left: 60, accounts: 2, from: ["Hub"] }],
},
{
name: "one login reported by two hubs, counted once at its freshest reading",
sources: [
hub("Old", [{ id: "a.json", email: "a@example.com", limits: limitsAt(90) }]),
hub("New", [
{
id: "a.json",
email: "a@example.com",
limits: limitsAt(30, "2026-10-30T11:59:30.000Z"),
},
]),
],
rows: [{ label: "Session", left: 70, accounts: 1, from: ["Old"] }],
},
{
name: "accounts across hubs, skipping a hub that failed to read",
sources: [
hub("Home", [{ id: "a.json", email: "a@example.com", limits: limitsAt(20) }]),
hub("Work", [{ id: "b.json", email: "b@example.com", limits: limitsAt(40) }]),
hub("Down", [], "The hub answered with HTTP 404."),
],
rows: [{ label: "Session", left: 70, accounts: 2, from: ["Home", "Work"] }],
},
{
name: "only this driver's readable accounts",
sources: [
hub("Hub", [
{ id: "a.json", email: "a@example.com", limits: limitsAt(50) },
{ id: "c.json", email: "c@example.com", driver: codex, limits: limitsAt(0) },
{ id: "d.json", email: "d@example.com", limits: unsupported },
]),
],
rows: [{ label: "Session", left: 50, accounts: 1, from: ["Hub"] }],
},
{
name: "a login signed in natively and on a hub, counted once at its freshest reading",
sources: [hub("Hub", [{ id: "a.json", email: "a@example.com", limits: limitsAt(90) }])],
natives: [signedIn("claude-a", "a@example.com", limitsAt(30, "2026-10-30T11:59:30.000Z"))],
rows: [{ label: "Session", left: 70, accounts: 1, from: ["Signed-in accounts"] }],
},
{
name: "a signed-in account no hub holds, alongside the hub's",
sources: [hub("Hub", [{ id: "a.json", email: "a@example.com", limits: limitsAt(20) }])],
natives: [signedIn("claude-b", "b@example.com", limitsAt(60))],
rows: [{ label: "Session", left: 60, accounts: 2, from: ["Hub", "Signed-in accounts"] }],
},
])("pools $name when the instance reads no limits", ({ sources, natives, rows }) => {
expect(shown(unsupported, sources, natives)).toEqual({ kind: "pool", rows });
});

it("names the next reset that refills quota, skipping an unused account's 0% reset", () => {
const soon = "2026-10-30T12:10:00.000Z";
const later = "2026-10-30T14:00:00.000Z";
const resolved = resolveThreadDetailsLimits(
instanceId,
[claudeInstance(unsupported)],
[
hub("Hub", [
{ id: "a.json", email: "a@example.com", limits: limitsResettingAt(0, soon) },
{ id: "b.json", email: "b@example.com", limits: limitsResettingAt(80, later) },
]),
],
now,
);
if (resolved?.kind !== "pool") throw new Error("expected a pool");
const [window] = resolved.windows;
expect(window!.resets.map((reset) => reset.restoresPercent)).toEqual([0, 40]);
expect(nextPoolReset(window!, now)).toMatchObject({
at: Date.parse(later),
restoresPercent: 40,
});
});

it("keeps the instance's notice when no hub has a readable account for its driver", () => {
const sources = [
hub("Hub", [
{ id: "c.json", driver: codex, limits: limitsAt(10) },
{ id: "d.json", limits: unsupported },
]),
];
const natives = [signedIn("claude-b", "b@example.com", limitsAt(60))];
expect(shown(unsupported, sources, natives)).toEqual({
kind: "instance",
limits: unsupported,
});
});
});
91 changes: 91 additions & 0 deletions apps/web/src/components/chat/ThreadDetailsUsage.logic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
EnvironmentId,
type ProviderInstanceId,
type ServerProvider,
type ServerProviderUsageLimits,
type UsageLimitSourceSnapshots,
} from "@t3tools/contracts";
import {
collectLimitAccounts,
collectLimitPools,
collectProviderUsageLimits,
displayLimitWindows,
type LimitPoolWindow,
limitsNotice,
} from "@t3tools/shared/usageLimits";

export type ThreadDetailsLimits =
| { readonly kind: "instance"; readonly limits: ServerProviderUsageLimits }
| { readonly kind: "pool"; readonly windows: readonly LimitPoolWindow[] };

// collectLimitAccounts keys its input by environment; the card reads one, so any id serves.
const CARD_ENVIRONMENT = EnvironmentId.make("thread-details");

/**
* The limits the thread details card shows for one provider instance: its own, or, when it reads
* none (as when it sends requests through a CLIProxyAPI hub), the pool of its driver's accounts.
*/
export function resolveThreadDetailsLimits(
providerInstanceId: ProviderInstanceId | null,
providers: readonly ServerProvider[],
sources: UsageLimitSourceSnapshots,
now: number,
): ThreadDetailsLimits | null {
const provider = providers.find((entry) => entry.instanceId === providerInstanceId);
if (!provider) return null;
const report = collectProviderUsageLimits(provider.instanceId, providers, sources, now);
// Only this instance's data: the report keeps one instance per login, and any other entry may be
// a different login or workspace, so fall back to the instance's own snapshot.
const own =
report?.accounts.find((entry) => entry.instanceId === provider.instanceId)?.limits ??
provider.usageLimits;
if (!own) return null;
if (limitsNotice(own) === null) return { kind: "instance", limits: own };
return driverPool(provider.driver, providers, sources, now) ?? { kind: "instance", limits: own };
}

/** The reset the card names: the soonest for one account, the soonest that refills quota for a pool. */
export function nextPoolReset(window: LimitPoolWindow, now: number) {
const pooled = window.members.length > 1;
return (
window.resets.find((reset) => reset.at > now && (!pooled || reset.restoresPercent > 0)) ?? null
);
}

function driverPool(
driver: ServerProvider["driver"],
providers: readonly ServerProvider[],
sources: UsageLimitSourceSnapshots,
now: number,
): ThreadDetailsLimits | null {
const hubReadsDriver = sources.some((source) =>
source.accounts.some(
(account) => account.driver === driver && limitsNotice(account.usageLimits) === null,
),
);
if (!hubReadsDriver) return null;
const presentations = new Map([
[
CARD_ENVIRONMENT,
{
entry: { target: { label: "" } },
serverConfig: { providers, usageLimitSources: sources },
},
],
]);
const accounts = collectLimitAccounts(presentations).filter(
(account) => account.driver === driver,
);
const [pool] = collectLimitPools(accounts, now);
if (!pool) return null;
return { kind: "pool", windows: displayLimitWindows(pool) };
}

/** Where a pooled window's readings come from: the hubs holding its hub-only accounts, then signed-in accounts. */
export function poolSources(window: LimitPoolWindow): string[] {
const hubs = window.members.flatMap(({ account }) =>
account.environments.length === 0 && account.sourceLabel ? [account.sourceLabel] : [],
);
const signedIn = window.members.some(({ account }) => account.environments.length > 0);
return [...new Set(hubs), ...(signedIn ? ["Signed-in accounts"] : [])];
}
Loading
Loading