Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
5 changes: 2 additions & 3 deletions apps/mobile/src/features/threads/ComposerUsageLimits.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { providerClients } from "@t3tools/client-runtime/provider-clients";
import type { EnvironmentId, UsageLimitsReport } from "@t3tools/contracts";
import { Pressable, ScrollView, View } from "react-native";

import { SymbolView } from "../../components/ui/AppSymbol";
import { AppText as Text } from "../../components/ui/AppText";
import { AccountLimits, ResetCredits } from "../usage/UsageLimitsSection";

const DRIVER_LABEL: Partial<Record<string, string>> = { codex: "Codex", claudeAgent: "Claude" };

export function ComposerUsageLimits({
report,
environmentId,
Expand Down Expand Up @@ -37,7 +36,7 @@ export function ComposerUsageLimits({
const resetCreditInput =
account.resetCreditInput ??
(account.instanceId ? { instanceId: account.instanceId } : undefined);
const driverLabel = DRIVER_LABEL[account.driver] ?? String(account.driver);
const driverLabel = providerClients.get(account.driver)?.label ?? account.driver;
return (
<AccountLimits
key={account.id}
Expand Down
15 changes: 6 additions & 9 deletions apps/mobile/src/features/usage/UsageLimitsPooled.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useAtomValue } from "@effect/atom-react";
import { useNavigation, type StaticScreenProps } from "@react-navigation/native";
import { EnvironmentId } from "@t3tools/contracts";
import { providerClients } from "@t3tools/client-runtime/provider-clients";
import { normalizeProviderAccentColor } from "@t3tools/client-runtime/state/provider-instance-display";
import {
collectLimitAccounts,
Expand Down Expand Up @@ -29,17 +30,11 @@ import { environmentPresentations } from "../../state/presentation";
import { ResetCredits } from "./UsageLimitsSection";
import { usageProviderKind, useProviderColors } from "./usageProviders";

const DRIVER_LABEL: Partial<Record<string, string>> = {
codex: "Codex",
claudeAgent: "Claude",
grok: "Grok",
antigravity: "Antigravity",
};
const PACE_LABEL = { ahead: "Ahead of pace", on: "On pace", under: "Under pace" } as const;

function accountName(account: LimitAccount) {
if (account.displayName) return account.displayName;
if (!account.email) return DRIVER_LABEL[account.driver] ?? String(account.driver);
if (!account.email) return providerClients.get(account.driver)?.label ?? account.driver;
return emailInitials(account.email) || "Account";
}

Expand Down Expand Up @@ -444,7 +439,7 @@ export function UsageLimitsSection({
<View className="flex-row items-center gap-2 px-1">
<ProviderIcon provider={pool.driver} size={18} />
<Text className="text-base font-t3-medium text-foreground">
{DRIVER_LABEL[pool.driver] ?? pool.driver}
{providerClients.get(pool.driver)?.label ?? pool.driver}
</Text>
</View>
{windows.map((window) => {
Expand Down Expand Up @@ -559,7 +554,9 @@ export function UsageLimitAccountScreen({ route }: AccountScreenProps) {
<View className="flex-row items-center gap-2">
<ProviderIcon provider={account.driver} size={24} />
<Text className="flex-1 text-xl font-t3-bold text-foreground">
{account.displayName ?? DRIVER_LABEL[account.driver] ?? account.driver}
{account.displayName ??
providerClients.get(account.driver)?.label ??
account.driver}
</Text>
</View>
{account.email ? (
Expand Down
33 changes: 19 additions & 14 deletions apps/server/src/provider/antigravityUsageLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,22 @@ function summaryResetTime(
return untouched ? undefined : bucket.resetTime;
}

const sha256 = Effect.fnUntraced(function* (text: string) {
const digest = yield* (yield* Digest.Digest).digest(
Digest.sha256,
new TextEncoder().encode(text),
);
return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
});

/** Keys an account by its Google ID, which matches across machines and hubs; refresh tokens do not. */
export const antigravityAccountFingerprint = (userInfo: unknown) =>
decodeUserInfo(userInfo).pipe(Effect.flatMap((info) => sha256(`google-account\0${info.id}`)));

/** Windows from a `retrieveUserQuotaSummary` answer requested at `requestedAt`. */
export const antigravitySummaryWindows = (response: unknown, requestedAt: number) =>
decodeSummary(response).pipe(Effect.map((summary) => summaryWindows(summary, requestedAt)));

function summaryWindows(summary: typeof Summary.Type, requestedAt: number) {
const windows = new Map<string, ServerProviderUsageWindow>();
const groups = summary.groups?.some((group) => group.buckets?.length)
Expand Down Expand Up @@ -197,15 +213,6 @@ export const makeAntigravityUsageProbe = Effect.fn("makeAntigravityUsageProbe")(
| undefined;
let generation = 0;

const sha256 = (text: string) =>
crypto
.digest(Digest.sha256, new TextEncoder().encode(text))
.pipe(
Effect.map((digest) =>
Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join(""),
),
);

const request = (request: HttpClientRequest.HttpClientRequest) =>
client.execute(request).pipe(
Effect.provideService(FetchHttpClient.RequestInit, MANUAL_REDIRECT),
Expand Down Expand Up @@ -253,8 +260,7 @@ export const makeAntigravityUsageProbe = Effect.fn("makeAntigravityUsageProbe")(
HttpClientRequest.setHeaders({ Authorization: `Bearer ${accessToken}` }),
),
).pipe(
Effect.flatMap(decodeUserInfo),
Effect.flatMap((info) => sha256(`google-account\0${info.id}`)),
Effect.flatMap(antigravityAccountFingerprint),
Effect.orElseSucceed(() => fingerprint),
);
if (generation === startedGeneration) {
Expand Down Expand Up @@ -307,9 +313,7 @@ export const makeAntigravityUsageProbe = Effect.fn("makeAntigravityUsageProbe")(
}
return [...windows.values()];
});
const summary = yield* fetchWindows("retrieveUserQuotaSummary", (response, requestedAt) =>
decodeSummary(response).pipe(Effect.map((summary) => summaryWindows(summary, requestedAt))),
);
const summary = yield* fetchWindows("retrieveUserQuotaSummary", antigravitySummaryWindows);
const windows = summary.length
? summary
: yield* fetchWindows("fetchAvailableModels", (response) =>
Expand All @@ -325,6 +329,7 @@ export const makeAntigravityUsageProbe = Effect.fn("makeAntigravityUsageProbe")(
return { ...makeUsageLimits({ checkedAt, windows }), credentialFingerprint: account };
});
const probe = read.pipe(
Effect.provideService(Digest.Digest, crypto),
Effect.timeout("30 seconds"),
Effect.catch(() =>
Effect.gen(function* () {
Expand Down
184 changes: 178 additions & 6 deletions apps/server/src/usage/cliproxyApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,15 @@ const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));

function fixture(
options: {
accounts?: Array<
(typeof accounts)[number] & {
disabled?: boolean;
id_token: { chatgpt_account_id: string; plan_type?: string; chatgpt_plan_type?: string };
}
>;
accounts?: Array<{
id: string;
auth_index: string;
provider: string;
email?: string;
disabled?: boolean;
project_id?: string;
id_token?: { chatgpt_account_id: string; plan_type?: string; chatgpt_plan_type?: string };
}>;
upstream?: (request: RequestBody) => { status: number; body: unknown };
cooldownStatus?: number;
} = {},
Expand Down Expand Up @@ -433,6 +436,175 @@ describe("CLIProxyAPI built-in management API", () => {
}),
);

const antigravitySummary = {
buckets: [
{
bucketId: "gemini-weekly",
window: "weekly",
remainingFraction: 0.75,
resetTime: "2099-01-01T00:00:00Z",
},
],
};

it.effect("reads an xAI account as Grok with Grok CLI headers", () =>
Effect.gen(function* () {
const test = fixture({
accounts: [{ id: "xai.json", auth_index: "x", provider: "xai", email: "x@example.com" }],
upstream: () => ({
status: 200,
body: {
config: {
creditUsagePercent: 40,
currentPeriod: { type: "USAGE_PERIOD_TYPE_WEEKLY", end: "2099-01-01T00:00:00Z" },
},
},
}),
});
const [account] = yield* (yield* test.api).readAccounts(config);
expect(account).toMatchObject({ driver: "grok", email: "x@example.com" });
expect(account?.usageLimits.windows).toMatchObject([
{ id: "subscription", kind: "weekly", usedPercent: 40 },
]);
const call = test.requests.find((request) => request.body?.url)?.body;
expect(call?.method).toBe("GET");
expect(call?.url).toBe("https://cli-chat-proxy.grok.com/v1/billing?format=credits");
expect(call?.header).toMatchObject({
"x-xai-token-auth": "xai-grok-cli",
"x-grok-client-version": expect.any(String),
"User-Agent": expect.stringMatching(/^grok-cli\//),
});
}),
);

it.effect("reads an Antigravity summary and keys the account by its Google ID", () =>
Effect.gen(function* () {
const test = fixture({
accounts: [
{
id: "antigravity-g@example.com.json",
auth_index: "g",
provider: "antigravity",
email: "g@example.com",
project_id: "project-7",
},
],
upstream: (request) => ({
status: 200,
body: request.url?.endsWith("/userinfo") ? { id: "google-42" } : antigravitySummary,
}),
});
const [account] = yield* (yield* test.api).readAccounts(config);
expect(account?.driver).toBe("antigravity");
expect(account?.email).toBeUndefined();
expect(account?.usageLimits.credentialFingerprint).toBe(
// sha256("google-account\0google-42"), as a local Antigravity probe reports it.
"136d4952771fd67a3600053f37492cbd05828712e0b1c89ebab03d4c22ab6554",
);
expect(account?.usageLimits.windows).toMatchObject([
{ id: "gemini-weekly", kind: "weekly", usedPercent: 25 },
]);
const summary = test.requests.find((request) => request.body?.method === "POST")?.body;
expect(summary?.url).toBe(
"https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary",
);
expect(summary?.data).toBe(encodeJson({ project: "project-7" }));
const identityCalls = test.requests.filter((request) =>
request.body?.url?.endsWith("/userinfo"),
);
expect(identityCalls).toHaveLength(1);
expect(identityCalls[0]?.body).toMatchObject({
method: "GET",
url: "https://www.googleapis.com/oauth2/v2/userinfo",
header: { Authorization: "Bearer $TOKEN$" },
});
}),
);

it.effect("keeps Antigravity limits and listing email when its identity read fails", () =>
Effect.gen(function* () {
const test = fixture({
accounts: [
{
id: "antigravity.json",
auth_index: "g",
provider: "antigravity",
email: "g@example.com",
project_id: "project-7",
},
],
upstream: (request) =>
request.url?.endsWith("/userinfo")
? { status: 403, body: { error: "denied" } }
: {
status: 200,
body: antigravitySummary,
},
});
const result = yield* (yield* test.api).readAccounts(config);
expect(result).toHaveLength(1);
const [account] = result;
expect(account).toMatchObject({ driver: "antigravity", email: "g@example.com" });
expect(account?.usageLimits.credentialFingerprint).toBeUndefined();
expect(account?.usageLimits.unavailable).toBeUndefined();
expect(account?.usageLimits.windows).toMatchObject([
{ id: "gemini-weekly", kind: "weekly", usedPercent: 25 },
]);
}),
);

it.effect.each([
{ name: "a refused xAI call", provider: "xai", status: 403, body: { error: "denied" } },
{ name: "a malformed xAI answer", provider: "xai", status: 200, body: { config: "x" } },
{ name: "a refused Antigravity call", provider: "antigravity", status: 401, body: {} },
{ name: "a malformed Antigravity answer", provider: "antigravity", status: 200, body: [] },
{ name: "an empty Antigravity summary", provider: "antigravity", status: 200, body: {} },
])("reports $name as unreadable", ({ provider, status, body }) =>
Effect.gen(function* () {
const test = fixture({
accounts: [{ id: `${provider}.json`, auth_index: "a", provider, project_id: "project-7" }],
upstream: () => ({ status, body }),
});
const [account] = yield* (yield* test.api).readAccounts(config);
expect(account?.usageLimits.unavailable?.reason).toBe("probeFailed");
}),
);

it.effect.each([undefined, "", " "])(
"rejects a missing or blank Antigravity project (%s) before upstream calls",
(project_id) =>
Effect.gen(function* () {
const test = fixture({
accounts: [
{
id: "antigravity.json",
auth_index: "a",
provider: "antigravity",
...(project_id === undefined ? {} : { project_id }),
},
],
});
const [account] = yield* (yield* test.api).readAccounts(config);
expect(account?.usageLimits.unavailable?.reason).toBe("probeFailed");
expect(test.requests.map((request) => request.path)).toEqual(["/v0/management/auth-files"]);
}),
);

it.effect("skips accounts of providers the hub table does not know", () =>
Effect.gen(function* () {
const test = fixture({
accounts: [
{ id: "kiro.json", auth_index: "k", provider: "kiro", email: "k@example.com" },
{ ...accounts[0]!, provider: "claude" },
],
upstream: () => ({ status: 200, body: { five_hour: { utilization: 1, resets_at: null } } }),
});
const result = yield* (yield* test.api).readAccounts(config);
expect(result.map((account) => account.driver)).toEqual(["claudeAgent"]);
expect(test.requests.map((request) => request.body?.auth_index)).not.toContain("k");
}),
);

it.effect("pins redemption to the displayed credit and clears only that account's cooldown", () =>
Effect.gen(function* () {
const test = fixture();
Expand Down
Loading
Loading