From 4e476b3fdd2540cf6343ec3307101d2a3a4324a9 Mon Sep 17 00:00:00 2001 From: Gi99lin <74502520+Gi99lin@users.noreply.github.com> Date: Thu, 21 May 2026 21:53:03 +0300 Subject: [PATCH] fix(usage): improve Claude and MiniMax plan label detection (#2498) Integrated into release/v3.8.2 --- open-sse/services/usage.ts | 88 +++++++++++++++---- .../usage/components/ProviderLimits/utils.tsx | 31 +++++-- tests/unit/provider-limits-ui.test.ts | 47 ++++++++++ tests/unit/usage-service-hardening.test.ts | 11 ++- 4 files changed, 156 insertions(+), 21 deletions(-) diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts index 4f2c7a9c62..3c98b53086 100644 --- a/open-sse/services/usage.ts +++ b/open-sse/services/usage.ts @@ -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 { + 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, @@ -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) ) { - return { message: getMiniMaxAuthErrorMessage(combinedMessage) }; + return { message: getMiniMaxAuthErrorMessage(apiStatusMessage || combinedMessage) }; } if (!response.ok) { @@ -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) { @@ -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, }; } @@ -2551,4 +2607,6 @@ export const __testing = { inferGitHubPlanName, getGeminiCliPlanLabel, getAntigravityPlanLabel, + getMiniMaxPlanLabel, + inferMiniMaxPlanLabelFromTotals, }; diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx index 6851c621e3..8a5a9dd057 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx @@ -45,15 +45,31 @@ function toRecord(value: unknown): Record { : {}; } +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_-]+/) @@ -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, ]; @@ -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()}` : ""; @@ -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 }; } diff --git a/tests/unit/provider-limits-ui.test.ts b/tests/unit/provider-limits-ui.test.ts index 653fbc1f89..735b5ec82d 100644 --- a/tests/unit/provider-limits-ui.test.ts +++ b/tests/unit/provider-limits-ui.test.ts @@ -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"); @@ -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); diff --git a/tests/unit/usage-service-hardening.test.ts b/tests/unit/usage-service-hardening.test.ts index 4e0cfccd65..74ac24a4bb 100644 --- a/tests/unit/usage-service-hardening.test.ts +++ b/tests/unit/usage-service-hardening.test.ts @@ -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) => { @@ -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", @@ -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); @@ -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); @@ -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 }],