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
88 changes: 73 additions & 15 deletions open-sse/services/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,63 @@ function shouldDisplayGitHubQuota(quota: UsageQuota | null): quota is UsageQuota
return quota.total > 0 || quota.remainingPercentage !== undefined;
}

function pickFirstNonEmptyString(...values: unknown[]): string | undefined {
for (const value of values) {
if (typeof value !== "string") continue;
const trimmed = value.trim();
if (trimmed) return trimmed;
}
return undefined;
}

function inferMiniMaxPlanLabelFromTotals(models: JsonRecord[]): string | null {
const maxSessionTotal = models.reduce(
(maxTotal, model) => Math.max(maxTotal, getMiniMaxSessionTotal(model)),
0
);

if (maxSessionTotal >= 15_000) return "Max";
if (maxSessionTotal >= 4_500) return "Plus";
if (maxSessionTotal >= 1_500) return "Starter";
return null;
}

function getMiniMaxPlanLabel(payload: JsonRecord, models: JsonRecord[] = []): string {
const raw = pickFirstNonEmptyString(
getFieldValue(payload, "current_subscribe_title", "currentSubscribeTitle"),
getFieldValue(payload, "plan_name", "planName"),
getFieldValue(payload, "plan", "plan"),
getFieldValue(payload, "current_plan_title", "currentPlanTitle"),
getFieldValue(payload, "combo_title", "comboTitle")
);

if (!raw) return inferMiniMaxPlanLabelFromTotals(models) || "Coding Plan";

const cleaned = raw
.replace(/^minimax\s+/i, "")
.replace(/\bcoding\s+plan\b/gi, "")
.replace(/\s{2,}/g, " ")
.trim();

return cleaned || inferMiniMaxPlanLabelFromTotals(models) || "Coding Plan";
}

function getClaudePlanLabel(...candidates: Array<string | null | undefined>): string | null {
for (const candidate of candidates) {
if (typeof candidate !== "string") continue;
const trimmed = candidate.trim();
if (
!trimmed ||
trimmed.toLowerCase() === "claude code" ||
trimmed.toLowerCase() === "unknown"
) {
continue;
}
return trimmed;
}
return null;
}

function createQuotaFromUsage(
usedValue: unknown,
totalValue: unknown,
Expand Down Expand Up @@ -353,16 +410,16 @@ async function getMiniMaxUsage(apiKey: string, provider: "minimax" | "minimax-cn
getFieldValue(baseResp, "status_msg", "statusMsg") ?? ""
).trim();
const combinedMessage = `${apiStatusMessage} ${rawText}`.trim();
const authLikeMessage =
const authLikeStatusMessage =
/token plan|coding plan|invalid api key|invalid key|unauthorized|inactive/i;

if (
response.status === 401 ||
response.status === 403 ||
apiStatusCode === 1004 ||
authLikeMessage.test(combinedMessage)
authLikeStatusMessage.test(apiStatusMessage)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Narrowed auth error detection scope

Changed from testing combinedMessage (status_msg + raw response text) to only apiStatusMessage. If the upstream API returns auth-related errors in other response fields or the raw body (but not in status_msg), they won't be detected as auth failures anymore.

The fallback at line 422 (apiStatusMessage || combinedMessage) helps with error message formatting but not with detection. Consider whether combinedMessage should still be tested as a fallback condition.

) {
return { message: getMiniMaxAuthErrorMessage(combinedMessage) };
return { message: getMiniMaxAuthErrorMessage(apiStatusMessage || combinedMessage) };
}

if (!response.ok) {
Expand Down Expand Up @@ -461,7 +518,7 @@ async function getMiniMaxUsage(apiKey: string, provider: "minimax" | "minimax-cn
return { message: "MiniMax connected. Unable to extract text quota usage." };
}

return { quotas };
return { plan: getMiniMaxPlanLabel(payload, textModels), quotas };
} catch (error) {
lastErrorMessage = (error as Error).message;
if (!canFallback) {
Expand Down Expand Up @@ -2043,21 +2100,20 @@ async function getClaudeUsage(accessToken?: string) {
}
}

// Try to extract plan tier from the OAuth response
const planRaw =
typeof data.tier === "string"
? data.tier
: typeof data.plan === "string"
? data.plan
: typeof data.subscription_type === "string"
? data.subscription_type
: null;
const bootstrap = await bootstrapPromise;
const plan =
getClaudePlanLabel(
typeof data.tier === "string" ? data.tier : null,
typeof data.plan === "string" ? data.plan : null,
typeof data.subscription_type === "string" ? data.subscription_type : null,
bootstrap?.organization_rate_limit_tier
) ?? undefined;

return {
plan: planRaw || "Claude Code",
...(plan ? { plan } : {}),
quotas,
extraUsage: data.extra_usage ?? null,
bootstrap: await bootstrapPromise,
bootstrap,
};
}

Expand Down Expand Up @@ -2551,4 +2607,6 @@ export const __testing = {
inferGitHubPlanName,
getGeminiCliPlanLabel,
getAntigravityPlanLabel,
getMiniMaxPlanLabel,
inferMiniMaxPlanLabelFromTotals,
};
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,31 @@ function toRecord(value: unknown): Record<string, unknown> {
: {};
}

function isClaudeOrganizationTypeLabel(value: string) {
return /^default_claude(?:_ai)?$/i.test(value.trim());
}

function normalizePlanCandidate(value: unknown) {
if (typeof value !== "string") return null;
const trimmed = value.trim();
if (!trimmed) return null;
if (trimmed.toLowerCase() === "unknown") return null;
if (PROVIDER_PLAN_FALLBACKS.has(trimmed.toLowerCase())) return null;
if (isClaudeOrganizationTypeLabel(trimmed)) return null;
return trimmed;
}

function escapeRegExpToken(token: string): string {
return token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

/** Match tier tokens as whole words (avoids MINIMAX → Max, APPROVE → Pro, etc.). */
function hasTierToken(upper: string, token: string): boolean {
const escaped = escapeRegExpToken(token.toUpperCase());
const pattern = new RegExp(`(?:^|[^A-Z])${escaped}(?:[^A-Z]|$)`);
return pattern.test(upper);
}

function toTitleCaseWords(value: string) {
return value
.split(/[\s_-]+/)
Expand Down Expand Up @@ -413,6 +429,7 @@ export function resolvePlanValue(plan, providerSpecificData) {
psd.accountTier,
// Claude OAuth bootstrap: rate_limit_tier has the Max 5x/20x multiplier.
psd.organizationRateLimitTier,
psd.rateLimitTier,
psd.organizationType,
];

Expand Down Expand Up @@ -443,7 +460,7 @@ export function normalizePlanTier(plan) {

// Match Anthropic bootstrap strings (claude_max, default_claude_max_20x, etc.)
// before the generic PRO/TEAM checks so underscored values don't fall through.
const claudeMatch = upper.match(/CLAUDE_(MAX|PRO|TEAM|ENTERPRISE|FREE)(?:_(\d+X))?/);
const claudeMatch = upper.match(/(?:DEFAULT_)?CLAUDE_(MAX|PRO|TEAM|ENTERPRISE|FREE)(?:_(\d+X))?/);
if (claudeMatch) {
const family = claudeMatch[1];
const multiplier = claudeMatch[2] ? ` ${claudeMatch[2].toLowerCase()}` : "";
Expand Down Expand Up @@ -489,19 +506,23 @@ export function normalizePlanTier(plan) {
return { key: "ultra", label: "Ultra", variant: "success", rank: 4, raw };
}

if (upper.includes("MAX")) {
if (hasTierToken(upper, "MAX")) {
return { key: "ultra", label: "Max", variant: "success", rank: 4, raw };
}

if (upper.includes("PRO") || upper.includes("PREMIUM")) {
if (hasTierToken(upper, "PRO") || hasTierToken(upper, "PREMIUM")) {
return { key: "pro", label: "Pro", variant: "success", rank: 3, raw };
}

if (upper.includes("LITE") || upper.includes("LIGHT")) {
if (hasTierToken(upper, "STARTER")) {
return { key: "lite", label: "Starter", variant: "primary", rank: 2, raw };
}

if (hasTierToken(upper, "LITE") || hasTierToken(upper, "LIGHT")) {
return { key: "lite", label: "Lite", variant: "primary", rank: 2, raw };
}

if (upper.includes("PLUS") || upper.includes("PAID")) {
if (hasTierToken(upper, "PLUS") || hasTierToken(upper, "PAID")) {
return { key: "plus", label: "Plus", variant: "success", rank: 2, raw };
}

Expand Down
47 changes: 47 additions & 0 deletions tests/unit/provider-limits-ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ test("provider plan fallbacks normalize to Unknown instead of repeating provider
assert.equal(tier.label, "Unknown");
});

test("tier token matching avoids substring false positives", () => {
assert.equal(providerLimitUtils.normalizePlanTier("MiniMax").key, "unknown");
assert.equal(providerLimitUtils.normalizePlanTier("APPROVE").key, "unknown");
assert.equal(providerLimitUtils.normalizePlanTier("Max").key, "ultra");
assert.equal(providerLimitUtils.normalizePlanTier("Pro").key, "pro");
});

test("paid individual tiers use non-gray badge variants", () => {
assert.equal(providerLimitUtils.normalizePlanTier("Plus").variant, "success");
assert.equal(providerLimitUtils.normalizePlanTier("Pro").variant, "success");
Expand Down Expand Up @@ -44,6 +51,46 @@ test("Claude providerSpecificData plan is used when live plan is missing", () =>
assert.equal(tier.variant, "success");
});

test("Claude bootstrap rate_limit_tier maps default_claude_max_20x to Max 20x", () => {
const resolvedPlan = providerLimitUtils.resolvePlanValue(null, {
organizationType: "default_claude_ai",
organizationRateLimitTier: "default_claude_max_20x",
});

assert.equal(resolvedPlan, "default_claude_max_20x");
const tier = providerLimitUtils.normalizePlanTier(resolvedPlan);
assert.equal(tier.key, "ultra");
assert.equal(tier.label, "Max 20x");
});

test("Claude organization_type default_claude_ai is ignored without rate_limit_tier", () => {
const resolvedPlan = providerLimitUtils.resolvePlanValue("Claude Code", {
organizationType: "default_claude_ai",
});

assert.equal(resolvedPlan, null);
const tier = providerLimitUtils.normalizePlanTier(resolvedPlan);
assert.equal(tier.label, "Unknown");
});

test("MiniMax coding plan titles map to tier badges", () => {
const proTier = providerLimitUtils.normalizePlanTier("MiniMax Coding Plan Pro");
assert.equal(proTier.key, "pro");
assert.equal(proTier.label, "Pro");

const starterTier = providerLimitUtils.normalizePlanTier("Starter");
assert.equal(starterTier.key, "lite");
assert.equal(starterTier.label, "Starter");

const minimaxOnly = providerLimitUtils.normalizePlanTier("MiniMax Coding Plan");
assert.notEqual(minimaxOnly.key, "ultra");
});

test("tier token matching ignores embedded substrings", () => {
assert.equal(providerLimitUtils.normalizePlanTier("APPROVE").key, "unknown");
assert.equal(providerLimitUtils.normalizePlanTier("LITERAL").key, "unknown");
});

test("remaining percentage helpers reflect remaining quota and stale resets refill to 100", () => {
assert.equal(providerLimitUtils.calculatePercentage(0, 100), 100);
assert.equal(providerLimitUtils.calculatePercentage(17, 100), 83);
Expand Down
11 changes: 10 additions & 1 deletion tests/unit/usage-service-hardening.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ test("usage service covers Claude default-plan fallback, legacy org denial and f
provider: "claude",
accessToken: "claude-default",
});
assert.equal(defaultPlan.plan, "Claude Code");
assert.equal(defaultPlan.plan, undefined);
assert.equal(defaultPlan.extraUsage, null);

globalThis.fetch = async (url) => {
Expand Down Expand Up @@ -1020,6 +1020,7 @@ test("usage service covers MiniMax usage parsing, documented endpoint fallback a
return new Response(
JSON.stringify({
base_resp: { status_code: 0, status_msg: "ok" },
plan_name: "MiniMax Coding Plan Lite",
model_remains: [
{
model_name: "MiniMax-M2.7",
Expand Down Expand Up @@ -1069,6 +1070,7 @@ test("usage service covers MiniMax usage parsing, documented endpoint fallback a
"https://api.minimax.io/v1/api/openplatform/coding_plan/remains",
]
);
assert.equal(usage.plan, "Lite");
assert.equal(usage.quotas["session (5h)"].used, 400);
assert.equal(usage.quotas["session (5h)"].total, 1500);
assert.equal(usage.quotas["session (5h)"].remaining, 1100);
Expand Down Expand Up @@ -1115,6 +1117,7 @@ test("usage service treats MiniMax token-plan counts as used usage", async () =>
apiKey: "minimax-key",
});

assert.equal(usage.plan, "Max");
assert.equal(usage.quotas["session (5h)"].used, 13);
assert.equal(usage.quotas["session (5h)"].remaining, 14987);
assert.equal(usage.quotas["session (5h)"].remainingPercentage, 99.91333333333333);
Expand Down Expand Up @@ -1258,6 +1261,12 @@ test("usage helper branches cover Gemini CLI and Antigravity plan label fallback
);

assert.equal(__testing.getAntigravityPlanLabel(null), "Free");
assert.equal(
__testing.getMiniMaxPlanLabel({}, [{ current_interval_total_count: 1500 }]),
"Starter"
);
assert.equal(__testing.getMiniMaxPlanLabel({}, [{ current_interval_total_count: 4500 }]), "Plus");
assert.equal(__testing.getMiniMaxPlanLabel({}, [{ current_interval_total_count: 15000 }]), "Max");
assert.equal(
__testing.getAntigravityPlanLabel({
allowedTiers: [{ id: "tier_pro", isDefault: true }],
Expand Down