8000 feat(plan): show Claude's renamed Max and Ultra plans and let Antigravity accounts pick Ultra 5x or 20x by InfinityPacer · Pull Request #64 · InfinityPacer/meterleaf · GitHub
[go: up one dir, main page]

Skip to content
Merged
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
4 changes: 2 additions & 2 deletions docs/reporting.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
- 归档账户不再出现在请求明细的账户筛选里。从归档列表打开某个账户的请求时,筛选仍保留这个账户并注明「已归档」,切回「全部账户」即可退出。
- 删除账户只移除账户展示,不会删除历史请求、累计统计,也不影响 Sub2API 或本机采集器。
- 重命名的名称只在 Meterleaf 中显示,最长 40 个字,不会改动上游。清空名称或选择恢复原名后,重新显示上游提供的名称。
- 套餐徽标按使用倍数分三档显示,同一档外观相同。Claude Pro、Claude Team Standard 与 ChatGPT Plus 为一档,Claude Max 5x、Claude Team Premium 与 ChatGPT Pro 5x 为二档,Claude Max 20x 与 ChatGPT Pro 20x 为三档。只知道订阅类型、不知道倍数时,例如只显示 Max 或 Team,不分档。无法识别的套餐只显示名称。
- 采集器读不到套餐时,例如账户已不在这台电脑上登录,可以在「设置套餐」里选择 Pro、Max 5x、Max 20x、Team Standard 或 Team Premium,选「自动识别」恢复采集器上报的套餐。设置只保存在 Meterleaf 中,不会被同步覆盖,也不会让本周期的额度预估重新起算。
- 套餐徽标按使用倍数分三档显示,同一档外观相同。Claude Pro、Claude Team Standard、ChatGPT Plus 与 Google AI Pro 为一档,Claude Max、Claude Team Premium、ChatGPT Pro 5x 与 Google AI Ultra 5x 为二档,Claude Ultra、ChatGPT Pro 20x 与 Google AI Ultra 20x 为三档。Claude 的 Max 与 Ultra 即原来的 Max 5x 与 Max 20x。只知道订阅类型、不知道倍数时,例如只显示 Max 或 Team,不分档。无法识别的套餐只显示名称。
- 采集器读不到套餐时,例如账户已不在这台电脑上登录,可以在「设置套餐」里选择 Pro、Max、Ultra、Team Standard 或 Team Premium,选「自动识别」恢复采集器上报的套餐。Antigravity 只报出 Google AI Ultra、不区分倍数,它的账户在「设置套餐」里选 Ultra 5x 或 Ultra 20x,徽标和档位随之显示。设置只保存在 Meterleaf 中,不会被同步覆盖,也不会让本周期的额度预估重新起算。
- 本地归档、删除状态和账户名称不会被同步覆盖。上游删除账户后,本地历史请求仍保留。

![账户合并示例](images/account-merge.png)
Expand Down
22 changes: 19 additions & 3 deletions src/collector/claude-code/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,9 @@ export function planFor(
return "team-premium";
if (credentialType !== "team") {
if (credentialTier === "default_claude_max_5x") return "max-5x";
if (credentialTier === "default_claude_max_20x") return "max-20x";
if (isUltraTier(credentialTier)) return "max-20x";
}
if (credentialType === "ultra") return "max-20x";
if (credentialType === "pro" || credentialType === "enterprise")
return credentialType;
const tier = account.rateLimitTier;
Expand All @@ -128,15 +129,30 @@ export function planFor(
? "team-premium"
: teamPlan(account.seatTier);
if (tier === "default_claude_max_5x") return "max-5x";
if (tier === "default_claude_max_20x") return "max-20x";
if (isUltraTier(tier) || account.organizationType === "claude_ultra")
return "max-20x";
if (account.organizationType === "claude_pro") return "pro";
if (account.seatTier?.startsWith("team_")) return teamPlan(account.seatTier);
if (credentialType === "team" || credentialType === "max")
return credentialType;
return account.organizationType;
}

const subscriptionTypes = new Set(["pro", "max", "team", "enterprise"]);
/**
* Ultra(原 Max 20x)仍以 max-20x 上报,已有账户的套餐与额度预估不随改名变化。上游目前仍写
* default_claude_max_20x,default_claude_ultra 是改名后可能出现的新取值。
*/
function isUltraTier(tier: string | null): boolean {
return tier === "default_claude_max_20x" || tier === "default_claude_ultra";
}

const subscriptionTypes = new Set([
"pro",
"max",
"ultra",
"team",
"enterprise",
]);

/**
* 订阅需有订阅计费方式,且组织类型属于 Claude 订阅。新版 Claude Code 不再写组织类型,
Expand Down
25 changes: 18 additions & 7 deletions src/shared/account-plan.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
/**
* 用户可为 Claude 账户手动指定的套餐。新版 Claude Code 只在登录凭据里保存套餐,
* 未登录的账户或凭据不可读时,由用户补上档位;取值与采集器上报的标识一致。
* 用户可手动指定的套餐,按平台列出。新版 Claude Code 只在登录凭据里保存套餐,未登录的账户或
* 凭据不可读时由用户补上档位;Antigravity 只报出 Google AI Ultra,不报 5x 或 20x,倍数由用户指定。
* 取值与采集器上报的标识一致。
*/
export const manualAccountPlansByPlatform = {
anthropic: ["pro", "max-5x", "max-20x", "team-standard", "team-premium"],
google: ["ultra-5x", "ultra-20x"],
} as const;

export type ManualPlanPlatform = keyof typeof manualAccountPlansByPlatform;

export const manualAccountPlans = [
"pro",
"max-5x",
"max-20x",
"team-standard",
"team-premium",
...manualAccountPlansByPlatform.anthropic,
...manualAccountPlansByPlatform.google,
] as const;

export type ManualAccountPlan = (typeof manualAccountPlans)[number];

export function isManualPlanPlatform(
platform: string | null | undefined,
): platform is ManualPlanPlatform {
return platform === "anthropic" || platform === "google";
}
12 changes: 6 additions & 6 deletions src/storage/ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
type QuotaPlanObservation,
} from "../domain/quota-plan";
import { quotaHistoryStart } from "../domain/quota";
import { isManualPlanPlatform } from "../shared/account-plan";

/** toISOString 的固定宽度格式,同格式的时间按文本比较即按时间先后。 */
const isoTimestampGlob =
Expand Down Expand Up @@ -1375,16 +1376,15 @@ export class LedgerStore {
}

/**
* 报表使用的账户资料。用户为 Claude 账户指定的套餐覆盖上游套餐,指定套餐即说明是订阅账户;
* 手动套餐只有 Claude 的取值,其他平台的账户不受影响。
* 报表使用的账户资料。用户为 Claude 或 Google 账户指定的套餐覆盖上游套餐,指定套餐即说明是
* 订阅账户;手动套餐只有这两个平台的取值,其他平台的账户不受影响。
*/
accounts(): SourceAccount[] {
const plans = this.accountPlans();
return this.sourceAccounts().map((account) => {
const plan =
account.platform === "anthropic"
? plans[ref(account.sourceId, account.externalId)]
: undefined;
const plan = isManualPlanPlatform(account.platform)
? plans[ref(account.sourceId, account.externalId)]
: undefined;
return plan
? {
...account,
Expand Down
8000
7 changes: 5 additions & 2 deletions src/web/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ import {
trendInsights,
unitNames,
} from "./lib/usage-summary";
import { isManualPlanPlatform } from "../shared/account-plan";
import { allTimeRange } from "../shared/date-range";
import { ReportTable } from "./components/ReportTable";
import { ModelDistribution } from "./components/ModelDistribution";
Expand Down Expand Up @@ -186,7 +187,6 @@ const initialFilter: ReportFilter = {
search: "",
};

/** 套餐名首字母大写;Claude 的 max-5x 这类档位显示为 Max 5x,未知套餐保留原文。 */
function readStoredUsdBasis(): UsdBasis | null {
try {
const value = localStorage.getItem("meterleaf-usd-basis");
Expand Down Expand Up @@ -975,7 +975,7 @@ export function App() {
<PencilLine size={16} />
重命名
</ActionMenu.Item>
{account.platform === "anthropic" &&
{isManualPlanPlatform(account.platform) &&
accountArchive.data?.plans && (
<ActionMenu.Item
className="account-menu-item"
Expand All @@ -984,12 +984,15 @@ export function App() {
accountArchive.mutation.isPending
}
=> {
const platform = account.platform;
if (!isManualPlanPlatform(platform)) return;
accountArchive.mutation.reset();
setAccountToPlan({
id: account.id,
name: account.name,
plan:
accountArchive.data?.plans?.[account.id] ?? null,
platform,
});
}}
>
Expand Down
19 changes: 14 additions & 5 deletions src/web/components/AccountPlanDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { useEffect, useState } from "react";
import { Dialog } from "@base-ui/react/dialog";
import { manualAccountPlans } from "../../shared/account-plan";
import {
manualAccountPlansByPlatform,
type ManualPlanPlatform,
} from "../../shared/account-plan";
import { planBadge } from "../lib/plan";
import { FilterSelect } from "./FilterSelect";
import { Button } from "./ui/button";
Expand All @@ -10,6 +13,7 @@ export interface PlanTarget {
name: string;
/** 用户已指定的套餐;未指定时为 null,使用自动识别结果。 */
plan: string | null;
platform: ManualPlanPlatform;
}

const automatic = "";
Expand Down Expand Up @@ -60,19 +64,24 @@ export function AccountPlanDialog({
withinDialog
options={[
{ value: automatic, label: "自动识别" },
...manualAccountPlans.map((plan) => ({
...(target
? manualAccountPlansByPlatform[target.platform]
: []
).map((plan) => ({
value: plan,
label: planBadge({
plan,
kind: "subscription",
platform: "anthropic",
platform: target!.platform,
})!.label,
})),
]}
/>
<Dialog.Description className="account-rename-hint">
自动识别使用采集器从 Claude Code
读到的套餐。账户不再登录这台电脑、读不到套餐时,可以在这里指定。设置后本周期的额度预估照常使用已有数据。
{target?.platform === "google"
? "Antigravity 只报出 Google AI Ultra,不区分 5x 和 20x,在这里指定订阅的倍数。"
: "自动识别使用采集器从 Claude Code 读到的套餐。账户不再登录这台电脑、读不到套餐时,可以在这里指定。"}
设置后本周期的额度预估照常使用已有数据。
</Dialog.Description>
{error && <p role="alert">{error}</p>}
<div className="account-rename-actions">
Expand Down
26 changes: 18 additions & 8 deletions src/web/lib/plan.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { LedgerAccount } from "../../shared/report";

/**
* 订阅档位按使用倍数对齐,不按套餐名称:Claude Pro 与 ChatGPT Plus 为 1 档,
* Claude Max 5x 与 ChatGPT Pro 5x 为 2 档,Claude Max 20x 与 ChatGPT Pro 20x
* 为 3 档。同名的 Pro 在两家上游处于不同档位,因此必须结合平台判断。
* 订阅档位按使用倍数对齐,不按套餐名称:Claude Pro、ChatGPT Plus 与 Google AI Pro 为 1 档,
* Claude Max(原 Max 5x)、ChatGPT Pro 5x 与 Google AI Ultra 5x 为 2 档,Claude Ultra
* (原 Max 20x)、ChatGPT Pro 20x 与 Google AI Ultra 20x 为 3 档。同名的 Pro 在各家上游处于
* 不同档位,因此必须结合平台判断。
*/
export type PlanTier = 1 | 2 | 3;

Expand All @@ -14,19 +15,22 @@ export interface PlanBadge {

/**
* 已知上游的套餐标识。Claude 采集器上报 pro、max-5x、max-20x、team-standard、team-premium,
* 只知道订阅类型时上报 max、team 或 enterprise;Sub2API 从
* ChatGPT 凭据读取 plan_type,其中 prolite 是 Pro 5x、pro 是 Pro 20x。
* 只知道订阅类型时上报 max、team 或 enterprise。2026-10-30 起 Anthropic 把 Max 5x 改名 Max、
* Max 20x 改名 Ultra,标识沿用原值,只改显示名。Sub2API 从 ChatGPT 凭据读取 plan_type,其中
* prolite 是 Pro 5x、pro 是 Pro 20x。Antigravity 采集器原样上报 Google AI Ultra 这类套餐名,
* 不带倍数,倍数由用户在套餐设置里指定为 ultra-5x 或 ultra-20x。
*/
const knownPlans: Record<string, Record<string, PlanBadge>> = {
anthropic: {
pro: { label: "Pro", tier: 1 },
"max-5x": { label: "Max 5x", tier: 2 },
"max-20x": { label: "Max 20x", tier: 3 },
// Team 席位按用量对齐,Standard 略高于 Pro,Premium 与 Max 5x 同一速率档位。
"max-5x": { label: "Max", tier: 2 },
"max-20x": { label: "Ultra", tier: 3 },
// Team 席位按用量对齐,Standard 略高于 Pro,Premium 与 Max(原 Max 5x)同一速率档位。
"team-standard": { label: "Team Standard", tier: 1 },
"team-premium": { label: "Team Premium", tier: 2 },
// 只知道订阅类型、不知道倍数时不给档位。
max: { label: "Max", tier: null },
ultra: { label: "Ultra", tier: 3 },
team: { label: "Team", tier: null },
enterprise: { label: "Enterprise", tier: null },
},
Expand All @@ -35,6 +39,12 @@ const knownPlans: Record<string, Record<string, PlanBadge>> = {
prolite: { label: "Pro 5x", tier: 2 },
pro: { label: "Pro 20x", tier: 3 },
},
google: {
"google ai pro": { label: "Pro", tier: 1 },
"google ai ultra": { label: "Ultra", tier: null },
"ultra-5x": { label: "Ultra 5x", tier: 2 },
"ultra-20x": { label: "Ultra 20x", tier: 3 },
},
// OpenCode Go 的额度不按订阅倍数分档,页面只显示名称。
"opencode-go": {
go: { label: "Go", tier: null },
Expand Down
21 changes: 21 additions & 0 deletions tests/claude-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ describe("Claude 套餐识别", () => {
[credential("pro", "default_claude_ai"), "pro"],
[credential("max", "default_claude_max_5x"), "max-5x"],
[credential("max", "default_claude_max_20x"), "max-20x"],
// 改名为 Ultra 后可能出现的取值,仍归到 max-20x,已有账户的套餐不变。
[credential("ultra", "default_claude_ultra"), "max-20x"],
[credential("ultra", null), "max-20x"],
[credential("max", null), "max"],
[credential("team", "default_claude_max_5x"), "team-premium"],
[credential("team", null), "team"],
Expand Down Expand Up @@ -592,4 +595,22 @@ describe("补上套餐后额度预估不丢已有数据", () => {
ledger.close();
}
});

test("Antigravity 账户指定 Ultra 5x 后,报表显示指定倍数,预估不变", () => {
const ledger = new LedgerStore(":memory:", defaultPriceBook);
try {
ledger.saveAccounts(
[{ ...account, platform: "google", plan: "Google AI Ultra" }],
at(0),
);
ledger.saveQuotas(quotas, at(200));
const before = estimate(ledger);
expect(before.usd).not.toBeNull();
ledger.setAccountPlan(ref("source", "team"), "ultra-5x");
expect(ledger.accounts()[0]?.plan).toBe("ultra-5x");
expect(estimate(ledger)).toEqual(before);
} finally {
ledger.close();
}
});
});
20 changes: 18 additions & 2 deletions tests/plan-badge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,25 @@ const subscription = (platform: string | undefined, plan: string) =>
test("plan tiers align by usage multiple across upstreams", () => {
expect(subscription("anthropic", "pro")).toEqual({ label: "Pro", tier: 1 });
expect(subscription("openai", "plus")).toEqual({ label: "Plus", tier: 1 });
// Anthropic 把 Max 5x 改名 Max、Max 20x 改名 Ultra,标识沿用原值。
expect(subscription("anthropic", "max-5x")).toEqual({
label: "Max 5x",
label: "Max",
tier: 2,
});
expect(subscription("openai", "prolite")).toEqual({
label: "Pro 5x",
tier: 2,
});
expect(subscription("anthropic", "max-20x")).toEqual({
label: "Max 20x",
label: "Ultra",
tier: 3,
});
expect(subscription("google", "ultra-5x")).toEqual({
label: "Ultra 5x",
tier: 2,
});
expect(subscription("google", "ultra-20x")).toEqual({
label: "Ultra 20x",
tier: 3,
});
// ChatGPT 的 pro 是 20x 档,与 Claude 的 Pro 同名但不同档。
Expand All @@ -42,3 +51,10 @@ test("OpenCode Go plans show their names without a usage tier", () => {
tier: null,
});
});

test("Antigravity reports Google AI Ultra without its multiple", () => {
expect(subscription("google", "Google AI Ultra")).toEqual({
label: "Ultra",
tier: null,
});
});
Loading
0