Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
18 changes: 18 additions & 0 deletions apps/mobile/src/features/usage/UsageLimitsPooled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,24 @@ export function UsageLimitsSection({
{providerClients.get(pool.driver)?.label ?? pool.driver}
</Text>
</View>
{pool.accounts
.filter((account) => account.limits.windows.length === 0)
.map((account) => (
<View
key={account.key}
className="flex-row items-center gap-3 rounded-[24px] border-continuous bg-grouped-card p-4"
>
<View className="size-5 items-center justify-center rounded-md bg-subtle-strong">
<Text className="text-xs font-t3-medium tabular-nums text-foreground">
{pool.accounts.indexOf(account) + 1}
</Text>
</View>
<Text className="min-w-0 flex-1 text-sm font-t3-medium text-foreground">
{accountName(account)}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
</Text>
<Text className="text-xs text-foreground-muted">Limits unavailable</Text>
</View>
))}
{windows.map((window) => {
const details =
pool.driver === "cursor" ? cursorUsageWindowDetails(window.id) : undefined;
Expand Down
58 changes: 57 additions & 1 deletion apps/mobile/src/widgets/subscriptionUsageSnapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { describe, expect, it, vi } from "vite-plus/test";
import {
buildSubscriptionUsageSnapshot,
subscriptionUsageTimeline,
createWidgetRefresher,
WIDGET_REFRESH_INTERVAL,
} from "./subscriptionUsageSnapshot";
Expand Down Expand Up @@ -99,6 +100,61 @@ describe("subscription widget snapshots", () => {
const snapshot = buildSubscriptionUsageSnapshot(input, deepLink);
expect(snapshot.providers[0]?.name).toBe("Codex");
expect(JSON.stringify(snapshot)).not.toContain("example.com");
const source = input.get(EnvironmentId.make("env"))!.serverConfig.usageLimitSources[0]!;
const failedInput = (windows: typeof limits.windows, includeHealthy = false) =>
new Map([
[
EnvironmentId.make("env"),
{
entry: { target: { label: "Remote" } },
serverConfig: {
providers: [],
usageLimitSources: [
{
...source,
accounts: [
...(includeHealthy ? source.accounts : []),
...source.accounts.map((account) => ({
...account,
id: "failed-account",
email: "failed@example.com",
usageLimits: {
checkedAt: windows.length > 0 ? checkedAt : "2026-09-05T11:00:00.000Z",
windows,
unavailable: { reason: "probeFailed" as const },
},
})),
],
},
],
},
},
],
]);
const failed = buildSubscriptionUsageSnapshot(failedInput([]), deepLink);
expect(failed.providers).toEqual([
{
name: "Codex",
detail: "No limits available",
windows: [],
expiresAt: 0,
totalWindows: 0,
},
]);
const partial = buildSubscriptionUsageSnapshot(failedInput([], true), deepLink);
expect(partial.providers[0]?.detail).toBe("Available subscription limits");
expect(partial.providers[0]?.windows[0]?.remaining).toBe(60);
expect(partial.checkedAt).toBe(now);
expect(partial.providers[0]?.expiresAt).toBe(now + 10 * 60_000);
expect(subscriptionUsageTimeline(partial, now)[0]?.props.providers[0]?.windows).toHaveLength(1);
const retained = buildSubscriptionUsageSnapshot(failedInput(limits.windows), deepLink);
expect(retained.providers[0]?.detail).toBe("Last known limits");
expect(retained.providers[0]?.windows[0]).toMatchObject({
label: "Stale · 5 hours",
remaining: 60,
});
expect(retained.checkedAt).toBe(now);
expect(retained.providers[0]?.expiresAt).toBe(now + 10 * 60_000);
});
it("bounds OS storage and puts the most constrained windows first", () => {
const windows = Array.from({ length: 20 }, (_, index) => ({
Expand Down Expand Up @@ -186,7 +242,7 @@ describe("subscription widget expiry", () => {
deepLink,
);
expect(snapshot.providers[0]).toMatchObject({
detail: "2 accounts · pooled",
detail: "Available subscription limits",
expiresAt,
windows: [{ remaining: 60, reset: expect.stringMatching(label) }],
});
Expand Down
32 changes: 25 additions & 7 deletions apps/mobile/src/widgets/subscriptionUsageSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ function subscriptionUsageProps(
): SubscriptionUsageSnapshot {
const pools = collectLimitPools(accounts, now);
const checked = accounts
.filter((account) => account.driver === "codex" || account.driver === "claudeAgent")
.filter(
(account) =>
account.limits.windows.length > 0 &&
(account.driver === "codex" || account.driver === "claudeAgent"),
)
.map((account) => Date.parse(account.limits.checkedAt));
return {
checkedAt: checked.length > 0 && checked.every(Number.isFinite) ? Math.min(...checked) : 0,
Expand All @@ -69,15 +73,19 @@ function subscriptionUsageProps(
.map((driver) => {
const pool = pools.find((candidate) => candidate.driver === driver);
const name = driver === "codex" ? "Codex" : "Claude";
if (!pool)
if (!pool || pool.windows.length === 0)
return {
name,
detail: "No limits available",
windows: [],
expiresAt: 0,
totalWindows: 0,
};
const checkedAt = Math.min(...pool.accounts.map((a) => Date.parse(a.limits.checkedAt)));
const checkedAt = Math.min(
...pool.accounts
.filter((account) => account.limits.windows.length > 0)
.map((account) => Date.parse(account.limits.checkedAt)),
);
const expiresAt = Math.min(
checkedAt + SNAPSHOT_MAX_AGE,
...pool.windows.flatMap((window) =>
Expand All @@ -102,15 +110,25 @@ function subscriptionUsageProps(
name,
detail: !fresh
? "Open T3 to refresh"
: pool.accounts.length > 1
? `${pool.accounts.length} accounts · pooled`
: "Subscription remaining",
: pool.accounts.some(
(account) =>
account.limits.windows.length > 0 &&
account.limits.unavailable?.reason === "probeFailed",
)
? "Last known limits"
: pool.accounts.length > 1
? "Available subscription limits"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
: "Subscription remaining",
expiresAt: fresh ? expiresAt : 0,
totalWindows: fresh ? pool.windows.length : 0,
windows: fresh
? selectedWindows.map((window) => ({
kind: window.kind,
label: window.label,
label: window.members.some(
(member) => member.account.limits.unavailable?.reason === "probeFailed",
)
? `Stale · ${window.label}`
: window.label,
remaining: Math.round(window.remainingPercent),
reset: window.resets[0]
? `${isUpcoming(window.resets[0]) ? "Next reset" : "Reset"} ${new Date(
Expand Down
175 changes: 171 additions & 4 deletions apps/server/src/usage/cliproxyApi.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
import * as HostProcess from "@t3tools/shared/HostProcess";
import { describe, expect, it } from "@effect/vitest";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import * as Schema from "effect/Schema";
import * as TestClock from "effect/testing/TestClock";
import { HttpClient, HttpClientError, HttpClientResponse } from "effect/http";
import { HttpClient, HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/http";

import { makeCliproxyApi } from "./cliproxyApi.ts";

Expand Down Expand Up @@ -60,11 +62,13 @@ function fixture(
}>;
upstream?: (request: RequestBody) => { status: number; body: unknown };
cooldownStatus?: number;
beforeCooldownReset?: Effect.Effect<void>;
beforeUpstreamResponse?: (request: RequestBody) => Effect.Effect<void>;
} = {},
) {
const requests: Array<{ path: string; body?: RequestBody }> = [];
const http = HttpClient.make((request) =>
Effect.sync(() => {
const respond = (request: HttpClientRequest.HttpClientRequest) =>
Effect.gen(function* () {
expect(request.headers.authorization).toBe("Bearer management-secret");
const path = new URL(request.url).pathname;
const body =
Expand Down Expand Up @@ -108,11 +112,19 @@ function fixture(
},
},
};
if (body && options.beforeUpstreamResponse) yield* options.beforeUpstreamResponse(body);
return HttpClientResponse.fromWeb(
request,
Response.json({ status_code: upstream.status, body: encodeJson(upstream.body) }),
);
}),
});
const http = HttpClient.make((request) =>
Effect.andThen(
request.url.endsWith("/reset-quota")
? (options.beforeCooldownReset ?? Effect.void)
: Effect.void,
respond(request),
),
);
return {
requests,
Expand Down Expand Up @@ -373,6 +385,161 @@ describe("CLIProxyAPI built-in management API", () => {
}),
);

it.effect(
"paces usage probes, retains recent readings through 429s, and expires them during backoff",
() =>
Effect.gen(function* () {
let limited = false;
let probes = 0;
const test = fixture({
accounts: [{ ...accounts[0]!, provider: "claude" }],
upstream: () => {
probes++;
return limited
? { status: 429, body: {} }
: { status: 200, body: { five_hour: { utilization: probes * 10, resets_at: null } } };
},
});
const api = yield* test.api;
const [initial] = yield* api.readAccounts(config);
expect(initial?.usageLimits.windows[0]?.usedPercent).toBe(10);
limited = true;
for (const minutes of [1, 2, 4, 8]) {
const previousProbes = probes;
yield* TestClock.adjust(minutes * 60_000 - 1);
expect((yield* api.readAccounts(config))[0]?.usageLimits).toMatchObject(
initial!.usageLimits,
);
expect(probes).toBe(previousProbes);
yield* TestClock.adjust(1);
const [account] = yield* api.readAccounts(config);
expect(probes).toBe(previousProbes + 1);
if (minutes < 8) {
expect(account?.usageLimits).toMatchObject(initial!.usageLimits);
expect(account?.usageLimits.unavailable?.message).toContain("last known");
} else {
expect(account?.usageLimits.windows).toEqual([]);
expect(account?.usageLimits.unavailable).toMatchObject({
reason: "probeFailed",
message: expect.stringContaining("rate limiting"),
});
}
}
limited = false;
yield* TestClock.adjust(8 * 60_000 - 1);
expect((yield* api.readAccounts(config))[0]?.usageLimits.unavailable?.message).toContain(
"rate limiting",
);
expect(probes).toBe(5);
yield* TestClock.adjust(1);
expect((yield* api.readAccounts(config))[0]?.usageLimits.windows[0]?.usedPercent).toBe(60);
yield* TestClock.adjust("1 minute");
expect((yield* api.readAccounts(config))[0]?.usageLimits.windows[0]?.usedPercent).toBe(70);
}),
);

it.effect(
"shares concurrent reads without blocking other identities or retaining a pre-reset reading",
() =>
Effect.gen(function* () {
const started = yield* Deferred.make<void>();
const release = yield* Deferred.make<void>();
const spent = yield* Deferred.make<void>();
let probes = 0;
const test = fixture({
accounts: [accounts[0]!],
upstream: (request) => ({
status: 200,
body: request.url?.endsWith("/consume")
? { code: "reset" }
: request.url?.endsWith("/rate-limit-reset-credits")
? { credits: [] }
: { rate_limit: { primary_window: { used_percent: ++probes * 10 } } },
}),
beforeUpstreamResponse: (request) =>
request.url?.endsWith("/wham/usage") && probes === 1
? Deferred.succeed(started, undefined).pipe(Effect.andThen(Deferred.await(release)))
: Effect.void,
beforeCooldownReset: Deferred.succeed(spent, undefined).pipe(Effect.asVoid),
});
const api = yield* test.api;
const first = yield* Effect.forkChild(api.readAccounts(config));
yield* Deferred.await(started);
const second = yield* Effect.forkChild(api.readAccounts(config), {
startImmediately: true,
});
const [other] = yield* api.readAccounts({ ...config, url: "http://other-hub.test:8317" });
expect(other?.usageLimits.windows[0]?.usedPercent).toBe(20);
const redeem = yield* Effect.forkChild(api.consume(config, "first.json", "credit"));
yield* Deferred.await(spent);
yield* Deferred.succeed(release, undefined);
expect((yield* Fiber.join(first))[0]?.usageLimits.windows[0]?.usedPercent).toBe(10);
expect((yield* Fiber.join(second))[0]?.usageLimits.windows[0]?.usedPercent).toBe(10);
expect((yield* Fiber.join(redeem)).outcome).toBe("reset");
const [fresh] = yield* api.readAccounts(config);
expect(fresh?.usageLimits.windows[0]?.usedPercent).toBe(30);
expect(probes).toBe(3);
}),
);

it.effect(
"retains pacing after a rejected reset but invalidates only the attempted account after success or uncertain failure",
() =>
Effect.gen(function* () {
let resetStatus = 429;
let limited = false;
let percent = 12;
const test = fixture({
upstream: (request) =>
request.url?.endsWith("/consume")
? { status: resetStatus, body: { code: "reset" } }
: limited
? { status: 429, body: {} }
: {
status: 200,
body: request.url?.endsWith("/rate-limit-reset-credits")
? { credits: [] }
: { rate_limit: { primary_window: { used_percent: percent } } },
},
});
const api = yield* test.api;
const usageProbes = () =>
test.requests.filter((request) => request.body?.url?.endsWith("/wham/usage")).length;
const initial = yield* api.readAccounts(config);
limited = true;
yield* TestClock.adjust("1 minute");
expect(yield* api.readAccounts(config)).toMatchObject(initial);
expect(usageProbes()).toBe(4);
expect((yield* Effect.flip(api.consume(config, "first.json", "credit"))).detail).toContain(
"rate limiting",
);
yield* TestClock.adjust("1 minute");
expect(yield* api.readAccounts(config)).toMatchObject(initial);
expect(usageProbes()).toBe(4);

resetStatus = 200;
limited = false;
percent = 0;
expect((yield* api.consume(config, "first.json", "credit")).outcome).toBe("reset");
const afterReset = yield* api.readAccounts(config);
expect(afterReset.map((account) => account.usageLimits.windows[0]?.usedPercent)).toEqual([
0, 12,
]);
expect(usageProbes()).toBe(5);

resetStatus = 503;
limited = true;
expect((yield* Effect.result(api.consume(config, "first.json", "next-credit")))._tag).toBe(
"Failure",
);
const afterFailure = yield* api.readAccounts(config);
expect(afterFailure[0]?.usageLimits.windows).toEqual([]);
expect(afterFailure[0]?.usageLimits.unavailable?.message).toContain("rate limiting");
expect(afterFailure[1]).toMatchObject(initial[1]!);
expect(usageProbes()).toBe(6);
}),
);

it.effect.each([
{
name: "spend budget",
Expand Down
Loading