Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
14 changes: 14 additions & 0 deletions apps/mobile/src/features/usage/UsageLimitsPooled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,20 @@ export function UsageLimitsSection({
{DRIVER_LABEL[pool.driver] ?? 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"
>
{account.email ? <AccountChip email={account.email} /> : null}
<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
37 changes: 37 additions & 0 deletions apps/mobile/src/widgets/subscriptionUsageSnapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,43 @@ 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 failed = buildSubscriptionUsageSnapshot(
new Map([
[
EnvironmentId.make("env"),
{
entry: { target: { label: "Remote" } },
serverConfig: {
providers: [],
usageLimitSources: [
{
...source,
accounts: source.accounts.map((account) => ({
...account,
usageLimits: {
checkedAt,
windows: [],
unavailable: { reason: "probeFailed" as const },
},
})),
},
],
},
},
],
]),
deepLink,
);
expect(failed.providers).toEqual([
{
name: "Codex",
detail: "No limits available",
windows: [],
expiresAt: 0,
totalWindows: 0,
},
]);
});
it("bounds OS storage and puts the most constrained windows first", () => {
const windows = Array.from({ length: 20 }, (_, index) => ({
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/widgets/subscriptionUsageSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ 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",
Expand Down
245 changes: 241 additions & 4 deletions apps/server/src/usage/cliproxyApi.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
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 @@ -56,11 +58,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 @@ -104,11 +108,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 @@ -369,6 +381,231 @@ 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("isolates cached readings by hub, file, provider, email, and workspace", () =>
Effect.gen(function* () {
const original = accounts[0]!;
const listed = [{ ...original }];
let probes = 0;
const test = fixture({
accounts: listed,
upstream: (request) => {
if (request.url?.endsWith("/rate-limit-reset-credits"))
return { status: 200, body: { credits: [] } };
const percent = ++probes * 10;
return {
status: 200,
body: {
five_hour: { utilization: percent, resets_at: null },
rate_limit: { primary_window: { used_percent: percent } },
},
};
},
});
const api = yield* test.api;
const baseline = yield* api.readAccounts(config);
const identities = [
{ url: "http://other-hub.test:8317", account: original },
{ url: config.url, account: { ...original, id: "replacement.json" } },
{ url: config.url, account: { ...original, provider: "claude" } },
{ url: config.url, account: { ...original, email: "replacement@example.com" } },
{
url: config.url,
account: { ...original, id_token: { chatgpt_account_id: "other-workspace" } },
},
];
for (const [index, identity] of identities.entries()) {
listed[0] = identity.account;
const [account] = yield* api.readAccounts({ ...config, url: identity.url });
expect(account).toMatchObject({
id: identity.account.id,
email: identity.account.email,
workspaceId: identity.account.id_token.chatgpt_account_id,
driver: identity.account.provider === "claude" ? "claudeAgent" : "codex",
});
expect(account?.usageLimits.windows[0]?.usedPercent).toBe((index + 2) * 10);
}
listed[0] = original;
expect(yield* api.readAccounts(config)).toEqual(baseline);
expect(probes).toBe(6);
}),
);

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("invalidates an account when reset is interrupted after the credit is spent", () =>
Effect.gen(function* () {
const spent = yield* Deferred.make<void>();
const test = fixture({
beforeCooldownReset: Deferred.succeed(spent, undefined).pipe(Effect.andThen(Effect.never)),
});
const api = yield* test.api;
yield* api.readAccounts(config);
const redeem = yield* Effect.forkChild(api.consume(config, "second.json", "credit-b"));
yield* Deferred.await(spent);
yield* Fiber.interrupt(redeem);
yield* api.readAccounts(config);
expect(
test.requests
.filter((request) => request.body?.url?.endsWith("/wham/usage"))
.map((request) => request.body?.auth_index)
.sort(),
).toEqual(["a", "b", "b"]);
}),
);

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