mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 00:44:49 +00:00
fix(usage): report enterprise spending limits
This commit is contained in:
@@ -195,6 +195,9 @@ See the [public usage source reference](../public-docs/plugins/reference.md#usag
|
||||
contract, account and window identity, provider-derived period names, login fallback, and
|
||||
read-only credential rules. Usage adapters own the interpretation of provider fields; the app
|
||||
renders their names and resolves pins without provider-specific duration guesses.
|
||||
Claude's OAuth usage endpoint is undocumented. Extra-usage amounts are reported in minor currency
|
||||
units and displayed only for USD; the balance contract does not represent other currencies.
|
||||
Codex individual spending limits provide percentages, not dollar amounts.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -162,6 +162,61 @@ describe("claude usage source", () => {
|
||||
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
||||
);
|
||||
});
|
||||
it("reports USD extra usage monthly limit and used credits in dollars", async () => {
|
||||
writeClaudeCredentials(claudeHome, "at_valid");
|
||||
fetchApi = mockFetch(
|
||||
new Map([
|
||||
[
|
||||
"https://api.anthropic.com/api/oauth/usage",
|
||||
() =>
|
||||
jsonResponse({
|
||||
...makeClaudeResponse(),
|
||||
extra_usage: {
|
||||
is_enabled: true,
|
||||
monthly_limit: 5000,
|
||||
used_credits: 1250,
|
||||
currency: "USD",
|
||||
},
|
||||
}),
|
||||
],
|
||||
]),
|
||||
);
|
||||
|
||||
const claude = findProvider(await service().listUsage(), "claude");
|
||||
|
||||
expect(claude.balances).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "extra_usage_monthly",
|
||||
used: 12.5,
|
||||
remaining: 37.5,
|
||||
limit: 50,
|
||||
unit: "usd",
|
||||
}),
|
||||
]);
|
||||
expect(claude.details).toContainEqual(
|
||||
expect.objectContaining({ id: "extra_usage", value: "Enabled" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not report extra usage balances without explicit USD currency", async () => {
|
||||
writeClaudeCredentials(claudeHome, "at_valid");
|
||||
fetchApi = mockFetch(
|
||||
new Map([
|
||||
[
|
||||
"https://api.anthropic.com/api/oauth/usage",
|
||||
() =>
|
||||
jsonResponse({
|
||||
...makeClaudeResponse(),
|
||||
extra_usage: { monthly_limit: 5000, used_credits: 1250, currency: "EUR" },
|
||||
}),
|
||||
],
|
||||
]),
|
||||
);
|
||||
|
||||
const claude = findProvider(await service().listUsage(), "claude");
|
||||
|
||||
expect(claude.balances).toEqual([]);
|
||||
});
|
||||
|
||||
it("accepts a null Claude resets_at when a window has no scheduled reset", async () => {
|
||||
writeClaudeCredentials(claudeHome, "at_valid");
|
||||
|
||||
@@ -68,6 +68,9 @@ const ClaudeUsageResponseSchema = z.object({
|
||||
extra_usage: z
|
||||
.object({
|
||||
is_enabled: z.boolean().optional(),
|
||||
monthly_limit: ApiNumberSchema.nullish(),
|
||||
used_credits: ApiNumberSchema.nullish(),
|
||||
currency: z.string().nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
@@ -491,6 +494,26 @@ function toCredentialRecord(credentials: ClaudeCredentials): ClaudeCredentialRec
|
||||
: null;
|
||||
}
|
||||
|
||||
function extraUsageBalances(extraUsage: ClaudeUsageResponse["extra_usage"]) {
|
||||
if (extraUsage?.currency?.toUpperCase() !== "USD") return [];
|
||||
const used = extraUsage.used_credits == null ? undefined : extraUsage.used_credits / 100;
|
||||
const limit = extraUsage.monthly_limit == null ? undefined : extraUsage.monthly_limit / 100;
|
||||
if (used == null && limit == null) return [];
|
||||
return [
|
||||
{
|
||||
id: "extra_usage_monthly",
|
||||
label: "Extra usage",
|
||||
used,
|
||||
remaining: used != null && limit != null ? Math.max(0, limit - used) : undefined,
|
||||
limit,
|
||||
unit: "usd" as const,
|
||||
tone: toneFromUsedPct(
|
||||
used != null && limit != null && limit > 0 ? (used / limit) * 100 : null,
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export async function fetchUsage(
|
||||
input: UsageInput,
|
||||
fetchApi: typeof fetch = fetch,
|
||||
@@ -594,20 +617,21 @@ export async function fetchUsage(
|
||||
}
|
||||
|
||||
const details: UsageDetail[] = [];
|
||||
const extraUsageEnabled = resp.extra_usage?.is_enabled;
|
||||
if (extraUsageEnabled !== undefined) {
|
||||
const extraUsage = resp.extra_usage;
|
||||
if (extraUsage?.is_enabled !== undefined) {
|
||||
details.push({
|
||||
id: "extra_usage",
|
||||
label: "Extra usage",
|
||||
value: extraUsageEnabled ? "Enabled" : "Disabled",
|
||||
value: extraUsage.is_enabled ? "Enabled" : "Disabled",
|
||||
});
|
||||
}
|
||||
const balances = extraUsageBalances(extraUsage);
|
||||
|
||||
return {
|
||||
status: "available",
|
||||
planLabel: plan ?? undefined,
|
||||
windows,
|
||||
balances: [],
|
||||
balances,
|
||||
details,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -117,6 +117,38 @@ test("coerces credit balance and marks a 96 percent window dangerous", async ()
|
||||
]);
|
||||
});
|
||||
|
||||
test("reports enterprise individual spend limit percentage and reset", async () => {
|
||||
const report = await fetchUsage(authInput(fixtureHome), async () =>
|
||||
Response.json({
|
||||
plan_type: "enterprise",
|
||||
spend_control: {
|
||||
individual_limit: { used_percent: 5, remaining_percent: 95, reset_at: 1795996800 },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(report).toMatchObject({
|
||||
planLabel: "enterprise",
|
||||
windows: [
|
||||
expect.objectContaining({
|
||||
id: "individual_limit",
|
||||
label: "Individual spend limit",
|
||||
summary: true,
|
||||
usedPct: 5,
|
||||
remainingPct: 95,
|
||||
resetsAt: new Date(1795996800 * 1000).toISOString(),
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("omits individual spend limit without a percentage", async () => {
|
||||
const report = await fetchUsage(authInput(fixtureHome), async () =>
|
||||
Response.json({ spend_control: { individual_limit: { reset_at: 1795996800 } } }),
|
||||
);
|
||||
expect(report.windows).toEqual([]);
|
||||
});
|
||||
|
||||
test("summarizes the session and weekly windows by default, not code review", async () => {
|
||||
const report = await fetchUsage(
|
||||
authInput(fixtureHome),
|
||||
|
||||
@@ -34,6 +34,15 @@ const rateLimitSchema = z.object({
|
||||
primary_window: windowSchema.nullish(),
|
||||
secondary_window: windowSchema.nullish(),
|
||||
});
|
||||
const spendControlSchema = z.object({
|
||||
individual_limit: z
|
||||
.object({
|
||||
used_percent: number.nullish(),
|
||||
remaining_percent: number.nullish(),
|
||||
reset_at: number.nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
const responseSchema = z.object({
|
||||
plan_type: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
@@ -48,6 +57,7 @@ const responseSchema = z.object({
|
||||
)
|
||||
.nullish(),
|
||||
code_review_rate_limit: rateLimitSchema.nullish(),
|
||||
spend_control: spendControlSchema.nullish(),
|
||||
credits: z.object({ balance: number.optional() }).nullish(),
|
||||
});
|
||||
|
||||
@@ -227,6 +237,27 @@ export async function fetchUsage(
|
||||
},
|
||||
});
|
||||
}),
|
||||
...(usage.spend_control?.individual_limit
|
||||
? (() => {
|
||||
const limit = usage.spend_control.individual_limit;
|
||||
const usedPct =
|
||||
limit.used_percent ??
|
||||
(limit.remaining_percent != null ? 100 - limit.remaining_percent : undefined);
|
||||
if (usedPct === undefined && limit.remaining_percent == null) return [];
|
||||
return [
|
||||
{
|
||||
id: "individual_limit",
|
||||
label: "Individual spend limit",
|
||||
summary: true,
|
||||
usedPct,
|
||||
remainingPct: limit.remaining_percent,
|
||||
resetsAt:
|
||||
limit.reset_at != null ? new Date(limit.reset_at * 1000).toISOString() : null,
|
||||
tone: toneFromUsedPct(usedPct),
|
||||
},
|
||||
];
|
||||
})()
|
||||
: []),
|
||||
...usageWindows({
|
||||
rateLimit: usage.code_review_rate_limit,
|
||||
scope: { id: "code_review", label: "Code review" },
|
||||
|
||||
Reference in New Issue
Block a user